ai generated solutions to our ai generated problems
This commit is contained in:
@@ -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);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
+52
-20
@@ -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;
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
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.drawImage(renderTreeCanvas(), 0, 0);
|
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 (
|
||||||
|
|||||||
+25
-12
@@ -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,22 +596,33 @@ 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
|
||||||
|
|
||||||
|
async function animate() {
|
||||||
|
const [{ gsap }, { ScrollTrigger }] = await Promise.all([
|
||||||
|
import('gsap'),
|
||||||
|
import('gsap/ScrollTrigger'),
|
||||||
|
])
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
|
||||||
if (!isHome) {
|
if (!isHome) {
|
||||||
gsap.to(mover, {
|
tween = gsap.to(mover, {
|
||||||
x: dockPosition.x,
|
x: dockPosition.x,
|
||||||
y: dockPosition.y,
|
y: dockPosition.y,
|
||||||
duration: 0.56,
|
duration: 0.56,
|
||||||
ease: 'power3.out',
|
ease: 'power3.out',
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
})
|
})
|
||||||
return undefined
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tween = gsap.fromTo(
|
tween = gsap.fromTo(
|
||||||
mover,
|
mover,
|
||||||
{ x: homePosition.x, y: homePosition.y },
|
{ x: homePosition.x, y: homePosition.y },
|
||||||
{
|
{
|
||||||
@@ -631,17 +638,24 @@ function ThemeToggleMover({ dockPosition, homePosition, isHome, theme, onThemeCh
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
animate()
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user