273 lines
9.2 KiB
TypeScript
273 lines
9.2 KiB
TypeScript
import React, { forwardRef, useEffect, useRef } from "react";
|
|
|
|
const PX = 2;
|
|
const W = 500;
|
|
const H = 420;
|
|
const LEAF_FLOOR = H - 60;
|
|
|
|
|
|
function buildTree(seed: number) {
|
|
const woodGrid = new Uint8Array(W * H);
|
|
const leafGrid = new Uint8Array(W * H);
|
|
|
|
let s = seed;
|
|
function rand() {
|
|
s = (s * 16807 + 0) % 2147483647;
|
|
return (s & 0x7fffffff) / 0x7fffffff;
|
|
}
|
|
|
|
function setWood(x: number, y: number, shade: number) {
|
|
const px = Math.round(x), py = Math.round(y);
|
|
if (px >= 0 && px < W && py >= 0 && py < H) {
|
|
const idx = py * W + px;
|
|
if (woodGrid[idx] === 0) woodGrid[idx] = shade;
|
|
}
|
|
}
|
|
|
|
function setLeaf(x: number, y: number, shade: number) {
|
|
const px = Math.round(x), py = Math.round(y);
|
|
if (px >= 0 && px < W && py >= 0 && py < LEAF_FLOOR) {
|
|
const idx = py * W + px;
|
|
if (shade > leafGrid[idx]) leafGrid[idx] = shade;
|
|
}
|
|
}
|
|
|
|
function drawLeaf(x: number, y: number, shade: number) {
|
|
const px = Math.round(x), py = Math.round(y);
|
|
setLeaf(px, py, shade);
|
|
const r = rand();
|
|
if (r < 0.60) setLeaf(px + (rand() > 0.5 ? 1 : -1), py, shade);
|
|
if (r < 0.32) setLeaf(px, py + (rand() > 0.45 ? -1 : 1), shade);
|
|
}
|
|
|
|
function leafScatter(cx: number, cy: number, radius: number, count: number) {
|
|
for (let i = 0; i < count; i++) {
|
|
const a = rand() * Math.PI * 2;
|
|
const r = Math.sqrt(rand()) * radius;
|
|
const lx = cx + Math.cos(a) * r * 0.5;
|
|
const vertStretch = 1.0 + (cy / H) * 3.0;
|
|
const ly = cy + Math.abs(Math.sin(a)) * r * vertStretch;
|
|
const yFr = (ly - cy) / radius;
|
|
const shade = yFr < 0.5 ? 3 : yFr < 1.2 ? 2 : 1;
|
|
drawLeaf(lx, ly, shade);
|
|
}
|
|
}
|
|
|
|
function drawLine(
|
|
x0: number, y0: number, x1: number, y1: number,
|
|
thickness: number, shade: number,
|
|
) {
|
|
const dx = x1 - x0, dy = y1 - y0;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
const steps = Math.max(Math.ceil(dist * 2), 1);
|
|
for (let i = 0; i <= steps; i++) {
|
|
const t = i / steps;
|
|
const bx = x0 + dx * t, by = y0 + dy * t;
|
|
const r = (thickness / 2) * (1 - t * 0.4);
|
|
for (let oy = -Math.ceil(r); oy <= Math.ceil(r); oy++)
|
|
for (let ox = -Math.ceil(r); ox <= Math.ceil(r); ox++)
|
|
if (ox * ox + oy * oy <= r * r) setWood(bx + ox, by + oy, shade);
|
|
}
|
|
}
|
|
|
|
function branch(
|
|
x: number, y: number,
|
|
angle: number, length: number,
|
|
thickness: number, depth: number,
|
|
) {
|
|
const endX = x + Math.cos(angle) * length;
|
|
const endY = y + Math.sin(angle) * length;
|
|
|
|
const woodShade = thickness > 5 ? 1 : thickness > 2.5 ? 2 : 3;
|
|
drawLine(x, y, endX, endY, thickness, woodShade);
|
|
|
|
if (depth <= 0 || length < 1.5) {
|
|
const r = 4 + rand() * 6;
|
|
|
|
const normalised = ((angle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
|
|
const deg = normalised * (180 / Math.PI);
|
|
const isNearHorizontal = deg < 30 || deg > 150;
|
|
|
|
if (isNearHorizontal) {
|
|
for (let i = 0; i < Math.round(r * r * 0.8); i++) {
|
|
const ox = (rand() - 0.5) * r * 2;
|
|
const oy = -rand() * r * 2.5;
|
|
const shade = oy < -r ? 3 : 2;
|
|
drawLeaf(endX + ox, endY + oy, shade);
|
|
}
|
|
} else {
|
|
leafScatter(endX, endY, r, Math.round(r * r * 0.6));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (depth === 1) {
|
|
const steps = Math.ceil(length / 5);
|
|
for (let i = 0; i <= steps; i++) {
|
|
const t = i / steps;
|
|
const mx = x + Math.cos(angle) * length * t;
|
|
const my = y + Math.sin(angle) * length * t;
|
|
leafScatter(mx, my, 3 + rand() * 2, 8 + Math.round(rand() * 6));
|
|
}
|
|
}
|
|
|
|
const numChildren = depth > 4 ? 2 : 3 + (rand() > 0.6 ? 1 : 0);
|
|
for (let i = 0; i < numChildren; i++) {
|
|
const spread = 0.30 + rand() * 0.20;
|
|
const angleOffset = (i - (numChildren - 1) / 2) * spread;
|
|
const droopBias = 0;
|
|
const childAngle = angle + angleOffset + (rand() - 0.5) * 0.1 + droopBias;
|
|
const lenFactor = 0.60 + rand() * 0.12;
|
|
const thickFactor = 0.50 + rand() * 0.10;
|
|
branch(endX, endY, childAngle, length * lenFactor,
|
|
Math.max(1, thickness * thickFactor), depth - 1);
|
|
}
|
|
|
|
if (depth > 1 && rand() > 0.25) {
|
|
const t = 0.25 + rand() * 0.35;
|
|
const sx = x + Math.cos(angle) * length * t;
|
|
const sy = y + Math.sin(angle) * length * t;
|
|
const side = rand() > 0.5 ? 1 : -1;
|
|
branch(sx, sy,
|
|
angle + side * (0.30 + rand() * 0.35), length * 0.5,
|
|
Math.max(1, thickness * 0.45), depth - 2);
|
|
}
|
|
}
|
|
|
|
const cx = W / 2;
|
|
const trunkTop = H - 150;
|
|
const base = H - 50;
|
|
|
|
|
|
for (let ty = trunkTop; ty <= base; ty++) {
|
|
const t = (ty - trunkTop) / (base - trunkTop);
|
|
const width = 10 + t * t * 16;
|
|
const hw = Math.ceil(width);
|
|
for (let ox = -hw; ox <= hw; ox++) {
|
|
const px = Math.round(cx + ox), py = ty;
|
|
if (px < 0 || px >= W || py < 0 || py >= H) continue;
|
|
woodGrid[py * W + px] = 2;
|
|
}
|
|
}
|
|
|
|
|
|
branch(cx - 30, trunkTop + 8, -Math.PI * 0.95, 70, 11, 7);
|
|
branch(cx - 22, trunkTop + 4, -Math.PI * 0.87, 68, 10, 7);
|
|
branch(cx - 14, trunkTop, -Math.PI * 0.78, 66, 10, 7);
|
|
branch(cx - 8, trunkTop - 4, -Math.PI * 0.68, 64, 9, 7);
|
|
branch(cx - 8, trunkTop + 4, -Math.PI * 0.70, 71, 10, 7);
|
|
branch(cx - 4, trunkTop - 7, -Math.PI * 0.60, 62, 9, 7);
|
|
branch(cx, trunkTop - 6, -Math.PI * 0.55, 70, 9, 7);
|
|
branch(cx, trunkTop - 7, -Math.PI * 0.50, 73, 9, 7);
|
|
branch(cx, trunkTop - 7, -Math.PI * 0.55, 77, 9, 7);
|
|
branch(cx, trunkTop - 6, -Math.PI * 0.45, 70, 9, 7);
|
|
branch(cx, trunkTop - 6, -Math.PI * 0.60, 70, 9, 7);
|
|
branch(cx, trunkTop - 6, -Math.PI * 0.40, 70, 9, 7);
|
|
branch(cx + 30, trunkTop + 8, -Math.PI * 0.1, 65, 11, 7);
|
|
branch(cx + 22, trunkTop + 4, -Math.PI * 0.18, 65, 10, 7);
|
|
branch(cx + 14, trunkTop, -Math.PI * 0.22, 63, 10, 7);
|
|
branch(cx + 14, trunkTop, -Math.PI * 0.25, 65, 10, 7);
|
|
branch(cx + 8, trunkTop - 4, -Math.PI * 0.32, 61, 9, 7);
|
|
branch(cx + 4, trunkTop - 7, -Math.PI * 0.40, 62, 9, 7);
|
|
branch(cx + 2, trunkTop + 4, -Math.PI * 0.45, 67, 10, 7);
|
|
branch(cx + 2, trunkTop + 5, -Math.PI * 0.17, 67, 10, 7);
|
|
branch(cx - 10, trunkTop + 20, -Math.PI * 0.75, 45, 7, 6);
|
|
branch(cx - 5, trunkTop + 20, -Math.PI * 0.62, 42, 6, 6);
|
|
branch(cx, trunkTop + 18, -Math.PI * 0.50, 44, 7, 6);
|
|
branch(cx + 10, trunkTop + 20, -Math.PI * 0.25, 45, 7, 6);
|
|
branch(cx + 5, trunkTop + 20, -Math.PI * 0.38, 42, 6, 6);
|
|
branch(cx + 3, trunkTop - 6, -Math.PI * 0.68, 10, 9, 7);
|
|
branch(cx - 3, trunkTop - 7, -Math.PI * 0.32, 10, 9, 7);
|
|
branch(cx + 3, trunkTop - 6, -Math.PI * 0.7, 5, 9, 7);
|
|
branch(cx - 3, trunkTop - 7, -Math.PI * 0.3, 5, 9, 7);
|
|
branch(cx + 3, trunkTop - 6, -Math.PI * 0.73, 20, 9, 7);
|
|
branch(cx, trunkTop - 3, -Math.PI * 0.15, 90, 5, 3);
|
|
|
|
|
|
|
|
return { woodGrid, leafGrid };
|
|
}
|
|
const WOOD_COLORS = ['', '#c96303', '#c96303', '#c96303', '#321901'];
|
|
const LEAF_COLORS = ['', '#ed5145', '#fdb068', '#fee5cd'];
|
|
|
|
export const TRUNK_TOP_CSS = (H - 150) + 10;
|
|
|
|
const Tree = forwardRef<HTMLCanvasElement>(function Tree(_, ref) {
|
|
const internalRef = useRef<HTMLCanvasElement>(null);
|
|
const canvasRef = (ref as React.RefObject<HTMLCanvasElement>) ?? internalRef;
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current!;
|
|
const ctx = canvas.getContext("2d")!;
|
|
canvas.width = W * PX;
|
|
canvas.height = H * PX;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
const { woodGrid, leafGrid } = buildTree(55);
|
|
|
|
for (let y = 0; y < H; y++)
|
|
for (let x = 0; x < W; x++) {
|
|
const v = woodGrid[y * W + x];
|
|
if (v) {
|
|
ctx.fillStyle = WOOD_COLORS[v] ?? '#321901';
|
|
ctx.fillRect(x * PX, y * PX, PX, PX);
|
|
}
|
|
}
|
|
|
|
for (let y = 0; y < H; y++)
|
|
for (let x = 0; x < W; x++) {
|
|
const v = leafGrid[y * W + x];
|
|
if (v) {
|
|
ctx.fillStyle = LEAF_COLORS[v];
|
|
ctx.fillRect(x * PX, y * PX, PX, PX);
|
|
}
|
|
}
|
|
|
|
const GROUND_Y = H - 50;
|
|
const groundColors = ['#fee5cd', '#fdca9b', '#fdb068'];
|
|
const grassColors = ['#ed5145', '#fb7b04', '#c96303', '#f17c74'];
|
|
|
|
let gs = 12345;
|
|
function grassRand() {
|
|
gs = (gs * 16807 + 0) % 2147483647;
|
|
return (gs & 0x7fffffff) / 0x7fffffff;
|
|
}
|
|
|
|
for (let x = 0; x < W; x++) {
|
|
const wave = Math.sin(x * 0.05) * 3 + Math.sin(x * 0.12) * 2;
|
|
const groundTop = Math.round(GROUND_Y + wave);
|
|
for (let y = groundTop; y < H; y++) {
|
|
const depth = (y - groundTop) / (H - groundTop);
|
|
const ci = depth < 0.3 ? 0 : depth < 0.7 ? 1 : 2;
|
|
ctx.fillStyle = groundColors[ci];
|
|
ctx.fillRect(x * PX, y * PX, PX, PX);
|
|
}
|
|
}
|
|
|
|
for (let x = 0; x < W; x++) {
|
|
if (grassRand() > 0.35) continue;
|
|
const wave = Math.sin(x * 0.05) * 3 + Math.sin(x * 0.12) * 2;
|
|
const surfaceY = Math.round(GROUND_Y + wave);
|
|
const bladeH = Math.floor(grassRand() * 4) + 2;
|
|
const color = grassColors[Math.floor(grassRand() * grassColors.length)];
|
|
ctx.fillStyle = color;
|
|
for (let b = 0; b < bladeH; b++) {
|
|
ctx.fillRect(x * PX, (surfaceY - b - 1) * PX, PX, PX);
|
|
}
|
|
if (grassRand() > 0.6 && x + 1 < W) {
|
|
ctx.fillRect((x + 1) * PX, (surfaceY - 1) * PX, PX, PX);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="tree"
|
|
style={{ imageRendering: "pixelated" }}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export default Tree;
|