From 7cdc7c14261ed904229ad37fed29878a2cfbbcb7 Mon Sep 17 00:00:00 2001 From: Heidi Date: Sat, 16 May 2026 11:35:29 +0100 Subject: [PATCH] fix --- Tree/Tree.tsx | 132 +++++++++++++++++++++++++++--------------------- src/App.jsx | 135 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 164 insertions(+), 103 deletions(-) diff --git a/Tree/Tree.tsx b/Tree/Tree.tsx index bd0ffef..1a59b05 100644 --- a/Tree/Tree.tsx +++ b/Tree/Tree.tsx @@ -192,6 +192,81 @@ 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; @@ -202,62 +277,7 @@ const Tree = forwardRef(function Tree(_, ref) { 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); - } - } + ctx.drawImage(renderTreeCanvas(), 0, 0); }, []); return ( diff --git a/src/App.jsx b/src/App.jsx index 73392d3..7090c11 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import L from 'leaflet' import 'leaflet/dist/leaflet.css' -import Tree from '../Tree/Tree' +import Tree, { prewarmTreeCanvas } from '../Tree/Tree' import FallingLeaves from '../Tree/FallingLeaves' const numberFormat = new Intl.NumberFormat('en-GB') @@ -36,6 +36,7 @@ const analyticsPreferencesKey = 'tssbot.analyticsPreferences' const analyticsPreferencesCookie = 'tssbot_analytics_preferences' const analyticsVisitorKey = 'tssbot.analyticsVisitor' const analyticsConsentVersion = 3 +const liveRefreshMs = 15000 const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || '' @@ -518,7 +519,7 @@ function App() { function AppContent() { const [route, setRoute] = useState(() => parseRoute()) const [leaderboard, setLeaderboard] = useState({ status: 'idle', data: null, error: null }) - const [live, setLive] = useState({ status: 'idle', data: null, error: null }) + const [live, setLive] = useState({ status: 'idle', data: null, error: null, updatedAt: 0 }) const [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null }) const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null }) const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences()) @@ -536,6 +537,11 @@ function AppContent() { [leaderboard.data], ) const matches = live.data?.matches || [] + const liveRef = useRef(live) + + useEffect(() => { + liveRef.current = live + }, [live]) function navigate(path) { window.history.pushState({}, '', path) @@ -555,6 +561,20 @@ function AppContent() { return () => window.removeEventListener('scroll', onScroll) }, []) + useEffect(() => { + if (route.page === 'home') return + + const prewarm = () => { + renderPixelMountainsCanvas() + prewarmTreeCanvas() + } + const requestIdle = window.requestIdleCallback || ((callback) => window.setTimeout(callback, 250)) + const cancelIdle = window.cancelIdleCallback || window.clearTimeout + const id = requestIdle(prewarm) + + return () => cancelIdle(id) + }, [route.page]) + useEffect(() => { const title = route.page === 'team' && route.teamName @@ -747,17 +767,21 @@ function AppContent() { useEffect(() => { if (!['home', 'battle-logs'].includes(route.page)) return if (!teams.length) return + const currentLive = liveRef.current + if (currentLive.status === 'ready' && Date.now() - currentLive.updatedAt < liveRefreshMs) return const controller = new AbortController() setLive((current) => - current.status === 'ready' ? current : { status: 'loading', data: null, error: null }, + current.status === 'ready' + ? current + : { status: 'loading', data: null, error: null, updatedAt: current.updatedAt || 0 }, ) fetchRecentTssGames(teams, controller.signal) - .then((data) => setLive({ status: 'ready', data, error: null })) + .then((data) => setLive({ status: 'ready', data, error: null, updatedAt: Date.now() })) .catch((error) => { if (!controller.signal.aborted) { - setLive({ status: 'error', data: null, error: error.message }) + setLive({ status: 'error', data: null, error: error.message, updatedAt: Date.now() }) } }) @@ -771,13 +795,13 @@ function AppContent() { const controller = new AbortController() const timer = window.setInterval(() => { fetchRecentTssGames(teams, controller.signal) - .then((data) => setLive({ status: 'ready', data, error: null })) + .then((data) => setLive({ status: 'ready', data, error: null, updatedAt: Date.now() })) .catch((error) => { if (!controller.signal.aborted) { setLive((current) => ({ ...current, error: error.message })) } }) - }, 15000) + }, liveRefreshMs) return () => { window.clearInterval(timer) @@ -1771,52 +1795,53 @@ function RecentGamesSection({ live, matches, navigate }) { ) } -function PixelMountains() { - const canvasRef = useRef(null) +let cachedPixelMountainsCanvas = null - useEffect(() => { - const canvas = canvasRef.current - const ctx = canvas.getContext('2d') - const WORLD_W = 1920 - const WORLD_H = 900 +function renderPixelMountainsCanvas() { + if (cachedPixelMountainsCanvas) return cachedPixelMountainsCanvas - function interpolate(points, x) { - for (let i = 0; i < points.length - 1; i++) { - const [x0, y0] = points[i] - const [x1, y1] = points[i + 1] - if (x >= x0 && x <= x1) { - const t = (x - x0) / Math.max(1, x1 - x0) - return y0 + (y1 - y0) * t - } - } + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const WORLD_W = 1920 + const WORLD_H = 900 - return points.at(-1)[1] - } - - function drawMountain(points, color, jitter = 0) { - const width = WORLD_W - const height = WORLD_H - ctx.fillStyle = color - - for (let x = 0; x < width; x++) { - const wave = jitter - ? Math.sin(x * 0.08) * jitter + Math.sin(x * 0.021) * jitter * 1.8 - : 0 - const y = Math.round(interpolate(points, x) + wave) - ctx.fillRect(x, y, 1, height - y) + function interpolate(points, x) { + for (let i = 0; i < points.length - 1; i++) { + const [x0, y0] = points[i] + const [x1, y1] = points[i + 1] + if (x >= x0 && x <= x1) { + const t = (x - x0) / Math.max(1, x1 - x0) + return y0 + (y1 - y0) * t } } - function draw() { - const width = WORLD_W - const height = WORLD_H + return points.at(-1)[1] + } - canvas.width = WORLD_W - canvas.height = WORLD_H - ctx.imageSmoothingEnabled = false - ctx.clearRect(0, 0, WORLD_W, WORLD_H) + function drawMountain(points, color, jitter = 0) { + const width = WORLD_W + const height = WORLD_H + ctx.fillStyle = color - drawMountain( + for (let x = 0; x < width; x++) { + const wave = jitter + ? Math.sin(x * 0.08) * jitter + Math.sin(x * 0.021) * jitter * 1.8 + : 0 + const y = Math.round(interpolate(points, x) + wave) + ctx.fillRect(x, y, 1, height - y) + } + } + + function draw() { + const width = WORLD_W + const height = WORLD_H + + canvas.width = WORLD_W + canvas.height = WORLD_H + ctx.imageSmoothingEnabled = false + ctx.clearRect(0, 0, WORLD_W, WORLD_H) + + drawMountain( [ [0, height * 0.82], [width * 0.12, height * 0.73], @@ -1878,9 +1903,25 @@ function PixelMountains() { '#fdca9b', 1.4, ) - } + } - draw() + draw() + cachedPixelMountainsCanvas = canvas + return canvas +} + +function PixelMountains() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + const source = renderPixelMountainsCanvas() + const ctx = canvas.getContext('2d') + + canvas.width = source.width + canvas.height = source.height + ctx.imageSmoothingEnabled = false + ctx.drawImage(source, 0, 0) }, []) return