From 91a657522aeab6836393e168922df1fb221fcb2b Mon Sep 17 00:00:00 2001 From: Heidi Date: Mon, 15 Jun 2026 08:52:18 +0100 Subject: [PATCH] ai generated solutions to our ai generated problems --- frontend/Tree/FallingLeaves.tsx | 70 +++++++++++++++++++++------- frontend/Tree/Tree.tsx | 78 +++++++++++++++++++++---------- frontend/src/App.jsx | 81 +++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 73 deletions(-) diff --git a/frontend/Tree/FallingLeaves.tsx b/frontend/Tree/FallingLeaves.tsx index 34452f3..386495b 100644 --- a/frontend/Tree/FallingLeaves.tsx +++ b/frontend/Tree/FallingLeaves.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from "react"; import { TRUNK_TOP_CSS } from "./Tree"; -// this is a comment + interface Leaf { x: number; y: number; @@ -23,24 +23,35 @@ export default function FallingLeaves({ treeRef }: { treeRef: React.RefObject { + 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 () => { - cancelAnimationFrame(animId); - window.removeEventListener("resize", resize); + stop(); + intersectionObserver.disconnect(); + resizeObserver.disconnect(); + document.removeEventListener("visibilitychange", onVisibilityChange); }; }, []); diff --git a/frontend/Tree/Tree.tsx b/frontend/Tree/Tree.tsx index 1a59b05..00f1424 100644 --- a/frontend/Tree/Tree.tsx +++ b/frontend/Tree/Tree.tsx @@ -189,6 +189,23 @@ function buildTree(seed: number) { } const WOOD_COLORS = ['', '#c96303', '#c96303', '#c96303', '#321901']; 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; @@ -201,31 +218,37 @@ function renderTreeCanvas() { const ctx = canvas.getContext("2d")!; canvas.width = W * PX; canvas.height = H * PX; - ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.imageSmoothingEnabled = false; 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 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); - } + if (v) setPixel(x, y, WOOD_COLORS[v] ?? '#321901'); } 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); - } + if (v) setPixel(x, y, LEAF_COLORS[v]); } const GROUND_Y = H - 50; - const groundColors = ['#fee5cd', '#fdca9b', '#fdb068']; - const grassColors = ['#ed5145', '#fb7b04', '#c96303', '#f17c74']; let gs = 12345; function grassRand() { @@ -239,8 +262,7 @@ function renderTreeCanvas() { 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); + setPixel(x, y, GROUND_COLORS[ci]); } } @@ -249,16 +271,17 @@ function renderTreeCanvas() { 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; + const color = GRASS_COLORS[Math.floor(grassRand() * GRASS_COLORS.length)]; 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) { - 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; return canvas; } @@ -272,12 +295,21 @@ const Tree = forwardRef(function Tree(_, ref) { 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); + const canvas = canvasRef.current; + if (!canvas) return undefined; + + const draw = () => { + const ctx = canvas.getContext("2d"); + 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 ( diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e92e312..4eef2d1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,13 +1,9 @@ -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import L from 'leaflet' -import { gsap } from 'gsap' -import { ScrollTrigger } from 'gsap/ScrollTrigger' import 'leaflet/dist/leaflet.css' import Tree, { prewarmTreeCanvas } from '../Tree/Tree' import FallingLeaves from '../Tree/FallingLeaves' -gsap.registerPlugin(ScrollTrigger) - const numberFormat = new Intl.NumberFormat('en-GB') const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', @@ -600,48 +596,66 @@ function themeToggleDockPosition(navPill) { function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeChange }) { const moverRef = useRef(null) - useLayoutEffect(() => { + useEffect(() => { const mover = moverRef.current if (!mover) return undefined + let cancelled = false + let tween = null - if (!isHome) { - gsap.to(mover, { - x: dockPosition.x, - y: dockPosition.y, - duration: 0.56, - ease: 'power3.out', - overwrite: true, - }) - return undefined + async function animate() { + const [{ gsap }, { ScrollTrigger }] = await Promise.all([ + import('gsap'), + import('gsap/ScrollTrigger'), + ]) + if (cancelled) return + + gsap.registerPlugin(ScrollTrigger) + + 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( - 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, - }, - }, - ) + animate() return () => { - tween.scrollTrigger?.kill() - tween.kill() + cancelled = true + tween?.scrollTrigger?.kill() + tween?.kill() } }, [dockPosition.x, dockPosition.y, homePosition.x, homePosition.y, isHome]) + const initialPosition = isHome ? homePosition : dockPosition + return (
@@ -1488,7 +1502,6 @@ function AppContent() { dock: themeToggleDockPosition(navPillRef.current), home: defaultThemeTogglePosition(), }) - ScrollTrigger.refresh() }) }