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; let cachedTreeCanvas: HTMLCanvasElement | null = null; function renderTreeCanvas() { if (cachedTreeCanvas) return cachedTreeCanvas; const canvas = document.createElement("canvas"); 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); } } cachedTreeCanvas = canvas; return canvas; } export function prewarmTreeCanvas() { renderTreeCanvas(); } const Tree = forwardRef(function Tree(_, ref) { const internalRef = useRef(null); const canvasRef = (ref as React.RefObject) ?? 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); ctx.drawImage(renderTreeCanvas(), 0, 0); }, []); return ( ); }); export default Tree;