fix
This commit is contained in:
+76
-56
@@ -192,6 +192,81 @@ const LEAF_COLORS = ['', '#ed5145', '#fdb068', '#fee5cd'];
|
|||||||
|
|
||||||
export const TRUNK_TOP_CSS = (H - 150) + 10;
|
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 Tree = forwardRef<HTMLCanvasElement>(function Tree(_, ref) {
|
||||||
const internalRef = useRef<HTMLCanvasElement>(null);
|
const internalRef = useRef<HTMLCanvasElement>(null);
|
||||||
const canvasRef = (ref as React.RefObject<HTMLCanvasElement>) ?? internalRef;
|
const canvasRef = (ref as React.RefObject<HTMLCanvasElement>) ?? internalRef;
|
||||||
@@ -202,62 +277,7 @@ const Tree = forwardRef<HTMLCanvasElement>(function Tree(_, ref) {
|
|||||||
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.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(renderTreeCanvas(), 0, 0);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+88
-47
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
import 'leaflet/dist/leaflet.css'
|
import 'leaflet/dist/leaflet.css'
|
||||||
import Tree from '../Tree/Tree'
|
import Tree, { prewarmTreeCanvas } from '../Tree/Tree'
|
||||||
import FallingLeaves from '../Tree/FallingLeaves'
|
import FallingLeaves from '../Tree/FallingLeaves'
|
||||||
|
|
||||||
const numberFormat = new Intl.NumberFormat('en-GB')
|
const numberFormat = new Intl.NumberFormat('en-GB')
|
||||||
@@ -36,6 +36,7 @@ const analyticsPreferencesKey = 'tssbot.analyticsPreferences'
|
|||||||
const analyticsPreferencesCookie = 'tssbot_analytics_preferences'
|
const analyticsPreferencesCookie = 'tssbot_analytics_preferences'
|
||||||
const analyticsVisitorKey = 'tssbot.analyticsVisitor'
|
const analyticsVisitorKey = 'tssbot.analyticsVisitor'
|
||||||
const analyticsConsentVersion = 3
|
const analyticsConsentVersion = 3
|
||||||
|
const liveRefreshMs = 15000
|
||||||
|
|
||||||
const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
|
const turnstileSiteKey = import.meta.env.VITE_TURNSTILE_SITE_KEY || ''
|
||||||
|
|
||||||
@@ -518,7 +519,7 @@ function App() {
|
|||||||
function AppContent() {
|
function AppContent() {
|
||||||
const [route, setRoute] = useState(() => parseRoute())
|
const [route, setRoute] = useState(() => parseRoute())
|
||||||
const [leaderboard, setLeaderboard] = useState({ status: 'idle', data: null, error: null })
|
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 [uptime, setUptime] = useState({ status: 'idle', checks: [], history: [], updatedAt: null })
|
||||||
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
|
const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null })
|
||||||
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences())
|
||||||
@@ -536,6 +537,11 @@ function AppContent() {
|
|||||||
[leaderboard.data],
|
[leaderboard.data],
|
||||||
)
|
)
|
||||||
const matches = live.data?.matches || []
|
const matches = live.data?.matches || []
|
||||||
|
const liveRef = useRef(live)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
liveRef.current = live
|
||||||
|
}, [live])
|
||||||
|
|
||||||
function navigate(path) {
|
function navigate(path) {
|
||||||
window.history.pushState({}, '', path)
|
window.history.pushState({}, '', path)
|
||||||
@@ -555,6 +561,20 @@ function AppContent() {
|
|||||||
return () => window.removeEventListener('scroll', onScroll)
|
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(() => {
|
useEffect(() => {
|
||||||
const title =
|
const title =
|
||||||
route.page === 'team' && route.teamName
|
route.page === 'team' && route.teamName
|
||||||
@@ -747,17 +767,21 @@ function AppContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!['home', 'battle-logs'].includes(route.page)) return
|
if (!['home', 'battle-logs'].includes(route.page)) return
|
||||||
if (!teams.length) return
|
if (!teams.length) return
|
||||||
|
const currentLive = liveRef.current
|
||||||
|
if (currentLive.status === 'ready' && Date.now() - currentLive.updatedAt < liveRefreshMs) return
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
setLive((current) =>
|
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)
|
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) => {
|
.catch((error) => {
|
||||||
if (!controller.signal.aborted) {
|
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 controller = new AbortController()
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
fetchRecentTssGames(teams, controller.signal)
|
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) => {
|
.catch((error) => {
|
||||||
if (!controller.signal.aborted) {
|
if (!controller.signal.aborted) {
|
||||||
setLive((current) => ({ ...current, error: error.message }))
|
setLive((current) => ({ ...current, error: error.message }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 15000)
|
}, liveRefreshMs)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timer)
|
window.clearInterval(timer)
|
||||||
@@ -1771,52 +1795,53 @@ function RecentGamesSection({ live, matches, navigate }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PixelMountains() {
|
let cachedPixelMountainsCanvas = null
|
||||||
const canvasRef = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
function renderPixelMountainsCanvas() {
|
||||||
const canvas = canvasRef.current
|
if (cachedPixelMountainsCanvas) return cachedPixelMountainsCanvas
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
const WORLD_W = 1920
|
|
||||||
const WORLD_H = 900
|
|
||||||
|
|
||||||
function interpolate(points, x) {
|
const canvas = document.createElement('canvas')
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
const ctx = canvas.getContext('2d')
|
||||||
const [x0, y0] = points[i]
|
const WORLD_W = 1920
|
||||||
const [x1, y1] = points[i + 1]
|
const WORLD_H = 900
|
||||||
if (x >= x0 && x <= x1) {
|
|
||||||
const t = (x - x0) / Math.max(1, x1 - x0)
|
|
||||||
return y0 + (y1 - y0) * t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return points.at(-1)[1]
|
function interpolate(points, x) {
|
||||||
}
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
const [x0, y0] = points[i]
|
||||||
function drawMountain(points, color, jitter = 0) {
|
const [x1, y1] = points[i + 1]
|
||||||
const width = WORLD_W
|
if (x >= x0 && x <= x1) {
|
||||||
const height = WORLD_H
|
const t = (x - x0) / Math.max(1, x1 - x0)
|
||||||
ctx.fillStyle = color
|
return y0 + (y1 - y0) * t
|
||||||
|
|
||||||
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() {
|
return points.at(-1)[1]
|
||||||
const width = WORLD_W
|
}
|
||||||
const height = WORLD_H
|
|
||||||
|
|
||||||
canvas.width = WORLD_W
|
function drawMountain(points, color, jitter = 0) {
|
||||||
canvas.height = WORLD_H
|
const width = WORLD_W
|
||||||
ctx.imageSmoothingEnabled = false
|
const height = WORLD_H
|
||||||
ctx.clearRect(0, 0, WORLD_W, 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],
|
[0, height * 0.82],
|
||||||
[width * 0.12, height * 0.73],
|
[width * 0.12, height * 0.73],
|
||||||
@@ -1878,9 +1903,25 @@ function PixelMountains() {
|
|||||||
'#fdca9b',
|
'#fdca9b',
|
||||||
1.4,
|
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 <canvas ref={canvasRef} className="pixel-mountains" aria-hidden="true" />
|
return <canvas ref={canvasRef} className="pixel-mountains" aria-hidden="true" />
|
||||||
|
|||||||
Reference in New Issue
Block a user