split the project in 2

This commit is contained in:
Heidi
2026-05-29 18:55:56 +01:00
parent 8aadda2d72
commit aef2113198
523 changed files with 2929 additions and 10 deletions
+105
View File
@@ -0,0 +1,105 @@
import React, { useEffect, useRef } from "react";
import { TRUNK_TOP_CSS } from "./Tree";
// this is a comment
interface Leaf {
x: number;
y: number;
size: number;
speed: number;
drift: number;
rotation: number;
rotationSpeed: number;
opacity: number;
sway: number;
swaySpeed: number;
phase: number;
}
export default function FallingLeaves({ treeRef }: { treeRef: React.RefObject<HTMLCanvasElement | null> }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext("2d")!;
let animId: number;
const leaves: Leaf[] = [];
const MAX_LEAVES = 50;
function resize() {
const parent = canvas.parentElement;
const rect = parent?.getBoundingClientRect();
canvas.width = Math.max(1, Math.round(rect?.width || window.innerWidth));
canvas.height = Math.max(1, Math.round(rect?.height || window.innerHeight));
}
resize();
window.addEventListener("resize", resize);
function spawnLeaf(): Leaf {
const treeRect = treeRef.current?.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
const spawnY = treeRect ? treeRect.top - canvasRect.top + TRUNK_TOP_CSS : canvas.height * 0.4;
const spawnX = treeRect
? treeRect.left - canvasRect.left + Math.random() * treeRect.width * 0.8 + treeRect.width * 0.1
: Math.random() * canvas.width * 0.6 + canvas.width * 0.2;
return {
x: spawnX,
y: spawnY,
size: Math.random() * 4 + 2,
speed: Math.random() * 0.3 + 0.15,
drift: Math.random() * 0.5 - 0.25,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.02,
opacity: Math.random() * 0.25 + 0.08,
sway: Math.random() * 30 + 15,
swaySpeed: Math.random() * 0.008 + 0.003,
phase: Math.random() * Math.PI * 2,
};
}
let spawnTimer = 0;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
spawnTimer++;
if (spawnTimer > 30 && leaves.length < MAX_LEAVES) {
leaves.push(spawnLeaf());
spawnTimer = 0;
}
for (let i = leaves.length - 1; i >= 0; i--) {
const l = leaves[i];
l.y += l.speed;
l.x += l.drift + Math.sin(l.phase) * l.swaySpeed * l.sway * 0.05;
l.phase += l.swaySpeed;
l.rotation += l.rotationSpeed;
ctx.save();
ctx.translate(l.x, l.y);
ctx.rotate(l.rotation);
ctx.globalAlpha = l.opacity;
ctx.fillStyle = "#fdb068";
const s = Math.round(l.size);
ctx.fillRect(-s / 2, -s / 2, s, s);
ctx.restore();
if (l.y > canvas.height + 10) {
leaves.splice(i, 1);
}
}
animId = requestAnimationFrame(draw);
}
draw();
return () => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", resize);
};
}, []);
return <canvas ref={canvasRef} className="falling-leaves" />;
}
+292
View File
@@ -0,0 +1,292 @@
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<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);
ctx.drawImage(renderTreeCanvas(), 0, 0);
}, []);
return (
<canvas
ref={canvasRef}
className="tree"
style={{ imageRendering: "pixelated" }}
/>
);
});
export default Tree;