ai generated solutions to our ai generated problems

This commit is contained in:
Heidi
2026-06-15 08:52:18 +01:00
parent efe233667f
commit 91a657522a
3 changed files with 156 additions and 73 deletions
+54 -16
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { TRUNK_TOP_CSS } from "./Tree"; import { TRUNK_TOP_CSS } from "./Tree";
// this is a comment
interface Leaf { interface Leaf {
x: number; x: number;
y: number; y: number;
@@ -23,24 +23,35 @@ export default function FallingLeaves({ treeRef }: { treeRef: React.RefObject<HT
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
let animId: number; let animId: number;
const leaves: Leaf[] = []; const leaves: Leaf[] = [];
const MAX_LEAVES = 50; const MAX_LEAVES = 36;
const frameInterval = 1000 / 30;
let lastFrame = 0;
let isVisible = true;
let isRunning = false;
let treeBounds = { left: 0, top: 0, width: 0 };
let canvasBounds = { left: 0, top: 0 };
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function resize() { function measure() {
const parent = canvas.parentElement; const parent = canvas.parentElement;
const rect = parent?.getBoundingClientRect(); const rect = parent?.getBoundingClientRect();
canvas.width = Math.max(1, Math.round(rect?.width || window.innerWidth)); canvas.width = Math.max(1, Math.round(rect?.width || window.innerWidth));
canvas.height = Math.max(1, Math.round(rect?.height || window.innerHeight)); 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 treeRect = treeRef.current?.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect();
const spawnY = treeRect ? treeRect.top - canvasRect.top + TRUNK_TOP_CSS : canvas.height * 0.4; treeBounds = treeRect
const spawnX = treeRect ? { left: treeRect.left, top: treeRect.top, width: treeRect.width }
? treeRect.left - canvasRect.left + Math.random() * treeRect.width * 0.8 + treeRect.width * 0.1 : { left: canvasRect.left + canvas.width * 0.2, top: canvasRect.top + canvas.height * 0.4, width: canvas.width * 0.6 };
: Math.random() * canvas.width * 0.6 + canvas.width * 0.2; canvasBounds = { left: canvasRect.left, top: canvasRect.top };
}
measure();
const resizeObserver = new ResizeObserver(measure);
if (canvas.parentElement) resizeObserver.observe(canvas.parentElement);
if (treeRef.current) resizeObserver.observe(treeRef.current);
function spawnLeaf(): Leaf {
const spawnY = treeBounds.top - canvasBounds.top + TRUNK_TOP_CSS;
const spawnX = treeBounds.left - canvasBounds.left + Math.random() * treeBounds.width * 0.8 + treeBounds.width * 0.1;
return { return {
x: spawnX, x: spawnX,
y: spawnY, y: spawnY,
@@ -58,7 +69,11 @@ export default function FallingLeaves({ treeRef }: { treeRef: React.RefObject<HT
let spawnTimer = 0; let spawnTimer = 0;
function draw() { function draw(timestamp: number) {
if (!isRunning) return;
animId = requestAnimationFrame(draw);
if (timestamp - lastFrame < frameInterval) return;
lastFrame = timestamp;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
spawnTimer++; spawnTimer++;
@@ -90,14 +105,37 @@ export default function FallingLeaves({ treeRef }: { treeRef: React.RefObject<HT
} }
} }
}
function start() {
if (reducedMotion || isRunning || !isVisible || document.hidden) return;
isRunning = true;
animId = requestAnimationFrame(draw); animId = requestAnimationFrame(draw);
} }
draw(); function stop() {
isRunning = false;
if (animId) cancelAnimationFrame(animId);
}
const intersectionObserver = new IntersectionObserver(([entry]) => {
isVisible = entry.isIntersecting;
if (isVisible) start();
else stop();
});
const onVisibilityChange = () => {
if (document.hidden) stop();
else start();
};
intersectionObserver.observe(canvas);
document.addEventListener("visibilitychange", onVisibilityChange);
start();
return () => { return () => {
cancelAnimationFrame(animId); stop();
window.removeEventListener("resize", resize); intersectionObserver.disconnect();
resizeObserver.disconnect();
document.removeEventListener("visibilitychange", onVisibilityChange);
}; };
}, []); }, []);
+55 -23
View File
@@ -189,6 +189,23 @@ function buildTree(seed: number) {
} }
const WOOD_COLORS = ['', '#c96303', '#c96303', '#c96303', '#321901']; const WOOD_COLORS = ['', '#c96303', '#c96303', '#c96303', '#321901'];
const LEAF_COLORS = ['', '#ed5145', '#fdb068', '#fee5cd']; const LEAF_COLORS = ['', '#ed5145', '#fdb068', '#fee5cd'];
const GROUND_COLORS = ['#fee5cd', '#fdca9b', '#fdb068'];
const GRASS_COLORS = ['#ed5145', '#fb7b04', '#c96303', '#f17c74'];
function hexToRgba(hex: string) {
return [
parseInt(hex.slice(1, 3), 16),
parseInt(hex.slice(3, 5), 16),
parseInt(hex.slice(5, 7), 16),
255,
];
}
const COLOR_RGBA = new Map(
[...WOOD_COLORS, ...LEAF_COLORS, ...GROUND_COLORS, ...GRASS_COLORS]
.filter(Boolean)
.map((color) => [color, hexToRgba(color)]),
);
export const TRUNK_TOP_CSS = (H - 150) + 10; export const TRUNK_TOP_CSS = (H - 150) + 10;
@@ -201,31 +218,37 @@ function renderTreeCanvas() {
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
canvas.width = W * PX; canvas.width = W * PX;
canvas.height = H * PX; canvas.height = H * PX;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.imageSmoothingEnabled = false;
const { woodGrid, leafGrid } = buildTree(55); const { woodGrid, leafGrid } = buildTree(55);
const pixelCanvas = document.createElement("canvas");
const pixelCtx = pixelCanvas.getContext("2d")!;
pixelCanvas.width = W;
pixelCanvas.height = H;
const image = pixelCtx.createImageData(W, H);
function setPixel(x: number, y: number, color: string) {
const rgba = COLOR_RGBA.get(color)!;
const index = (y * W + x) * 4;
image.data[index] = rgba[0];
image.data[index + 1] = rgba[1];
image.data[index + 2] = rgba[2];
image.data[index + 3] = rgba[3];
}
for (let y = 0; y < H; y++) for (let y = 0; y < H; y++)
for (let x = 0; x < W; x++) { for (let x = 0; x < W; x++) {
const v = woodGrid[y * W + x]; const v = woodGrid[y * W + x];
if (v) { if (v) setPixel(x, y, WOOD_COLORS[v] ?? '#321901');
ctx.fillStyle = WOOD_COLORS[v] ?? '#321901';
ctx.fillRect(x * PX, y * PX, PX, PX);
}
} }
for (let y = 0; y < H; y++) for (let y = 0; y < H; y++)
for (let x = 0; x < W; x++) { for (let x = 0; x < W; x++) {
const v = leafGrid[y * W + x]; const v = leafGrid[y * W + x];
if (v) { if (v) setPixel(x, y, LEAF_COLORS[v]);
ctx.fillStyle = LEAF_COLORS[v];
ctx.fillRect(x * PX, y * PX, PX, PX);
}
} }
const GROUND_Y = H - 50; const GROUND_Y = H - 50;
const groundColors = ['#fee5cd', '#fdca9b', '#fdb068'];
const grassColors = ['#ed5145', '#fb7b04', '#c96303', '#f17c74'];
let gs = 12345; let gs = 12345;
function grassRand() { function grassRand() {
@@ -239,8 +262,7 @@ function renderTreeCanvas() {
for (let y = groundTop; y < H; y++) { for (let y = groundTop; y < H; y++) {
const depth = (y - groundTop) / (H - groundTop); const depth = (y - groundTop) / (H - groundTop);
const ci = depth < 0.3 ? 0 : depth < 0.7 ? 1 : 2; const ci = depth < 0.3 ? 0 : depth < 0.7 ? 1 : 2;
ctx.fillStyle = groundColors[ci]; setPixel(x, y, GROUND_COLORS[ci]);
ctx.fillRect(x * PX, y * PX, PX, PX);
} }
} }
@@ -249,16 +271,17 @@ function renderTreeCanvas() {
const wave = Math.sin(x * 0.05) * 3 + Math.sin(x * 0.12) * 2; const wave = Math.sin(x * 0.05) * 3 + Math.sin(x * 0.12) * 2;
const surfaceY = Math.round(GROUND_Y + wave); const surfaceY = Math.round(GROUND_Y + wave);
const bladeH = Math.floor(grassRand() * 4) + 2; const bladeH = Math.floor(grassRand() * 4) + 2;
const color = grassColors[Math.floor(grassRand() * grassColors.length)]; const color = GRASS_COLORS[Math.floor(grassRand() * GRASS_COLORS.length)];
ctx.fillStyle = color;
for (let b = 0; b < bladeH; b++) { for (let b = 0; b < bladeH; b++) {
ctx.fillRect(x * PX, (surfaceY - b - 1) * PX, PX, PX); setPixel(x, surfaceY - b - 1, color);
} }
if (grassRand() > 0.6 && x + 1 < W) { if (grassRand() > 0.6 && x + 1 < W) {
ctx.fillRect((x + 1) * PX, (surfaceY - 1) * PX, PX, PX); setPixel(x + 1, surfaceY - 1, color);
} }
} }
pixelCtx.putImageData(image, 0, 0);
ctx.drawImage(pixelCanvas, 0, 0, canvas.width, canvas.height);
cachedTreeCanvas = canvas; cachedTreeCanvas = canvas;
return canvas; return canvas;
} }
@@ -272,12 +295,21 @@ const Tree = forwardRef<HTMLCanvasElement>(function Tree(_, ref) {
const canvasRef = (ref as React.RefObject<HTMLCanvasElement>) ?? internalRef; const canvasRef = (ref as React.RefObject<HTMLCanvasElement>) ?? internalRef;
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current!; const canvas = canvasRef.current;
const ctx = canvas.getContext("2d")!; if (!canvas) return undefined;
canvas.width = W * PX;
canvas.height = H * PX; const draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height); const ctx = canvas.getContext("2d");
ctx.drawImage(renderTreeCanvas(), 0, 0); if (!ctx) return;
canvas.width = W * PX;
canvas.height = H * PX;
ctx.drawImage(renderTreeCanvas(), 0, 0);
};
const requestIdle = window.requestIdleCallback ?? ((callback) => window.setTimeout(callback, 100));
const cancelIdle = window.cancelIdleCallback ?? window.clearTimeout;
const idleId = requestIdle(draw, { timeout: 700 });
return () => cancelIdle(idleId);
}, []); }, []);
return ( return (
+47 -34
View File
@@ -1,13 +1,9 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import L from 'leaflet' import L from 'leaflet'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import 'leaflet/dist/leaflet.css' import 'leaflet/dist/leaflet.css'
import Tree, { prewarmTreeCanvas } from '../Tree/Tree' import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
import FallingLeaves from '../Tree/FallingLeaves' import FallingLeaves from '../Tree/FallingLeaves'
gsap.registerPlugin(ScrollTrigger)
const numberFormat = new Intl.NumberFormat('en-GB') const numberFormat = new Intl.NumberFormat('en-GB')
const dateFormat = new Intl.DateTimeFormat('en-GB', { const dateFormat = new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium', dateStyle: 'medium',
@@ -600,48 +596,66 @@ function themeToggleDockPosition(navPill) {
function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeChange }) { function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeChange }) {
const moverRef = useRef(null) const moverRef = useRef(null)
useLayoutEffect(() => { useEffect(() => {
const mover = moverRef.current const mover = moverRef.current
if (!mover) return undefined if (!mover) return undefined
let cancelled = false
let tween = null
if (!isHome) { async function animate() {
gsap.to(mover, { const [{ gsap }, { ScrollTrigger }] = await Promise.all([
x: dockPosition.x, import('gsap'),
y: dockPosition.y, import('gsap/ScrollTrigger'),
duration: 0.56, ])
ease: 'power3.out', if (cancelled) return
overwrite: true,
}) gsap.registerPlugin(ScrollTrigger)
return undefined
if (!isHome) {
tween = gsap.to(mover, {
x: dockPosition.x,
y: dockPosition.y,
duration: 0.56,
ease: 'power3.out',
overwrite: true,
})
return
}
tween = gsap.fromTo(
mover,
{ x: homePosition.x, y: homePosition.y },
{
x: dockPosition.x,
y: dockPosition.y,
ease: 'none',
overwrite: true,
scrollTrigger: {
end: '+=96',
scrub: 0.35,
start: 'top top',
trigger: document.documentElement,
},
},
)
} }
const tween = gsap.fromTo( animate()
mover,
{ x: homePosition.x, y: homePosition.y },
{
x: dockPosition.x,
y: dockPosition.y,
ease: 'none',
overwrite: true,
scrollTrigger: {
end: '+=96',
scrub: 0.35,
start: 'top top',
trigger: document.documentElement,
},
},
)
return () => { return () => {
tween.scrollTrigger?.kill() cancelled = true
tween.kill() tween?.scrollTrigger?.kill()
tween?.kill()
} }
}, [dockPosition.x, dockPosition.y, homePosition.x, homePosition.y, isHome]) }, [dockPosition.x, dockPosition.y, homePosition.x, homePosition.y, isHome])
const initialPosition = isHome ? homePosition : dockPosition
return ( return (
<div <div
className="theme-toggle-mover fixed left-0 top-0 z-[60]" className="theme-toggle-mover fixed left-0 top-0 z-[60]"
ref={moverRef} ref={moverRef}
style={{ transform: `translate3d(${initialPosition.x}px, ${initialPosition.y}px, 0)` }}
> >
<ThemeToggle theme={theme} onThemeChange={onThemeChange} /> <ThemeToggle theme={theme} onThemeChange={onThemeChange} />
</div> </div>
@@ -1488,7 +1502,6 @@ function AppContent() {
dock: themeToggleDockPosition(navPillRef.current), dock: themeToggleDockPosition(navPillRef.current),
home: defaultThemeTogglePosition(), home: defaultThemeTogglePosition(),
}) })
ScrollTrigger.refresh()
}) })
} }