From 30d62e878afda14e19bdeefa7219dd9c3dca6668 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 10 Mar 2026 21:43:27 +0100 Subject: [PATCH] feat: add the waters --- frontend/src/app/platform/PlatformPage.tsx | 4 +- frontend/src/app/platform/ProjectsPage.tsx | 6 +- .../app/platform/ProjectsPageBackground.tsx | 99 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 frontend/src/app/platform/ProjectsPageBackground.tsx diff --git a/frontend/src/app/platform/PlatformPage.tsx b/frontend/src/app/platform/PlatformPage.tsx index eec5356..aa84797 100644 --- a/frontend/src/app/platform/PlatformPage.tsx +++ b/frontend/src/app/platform/PlatformPage.tsx @@ -25,8 +25,8 @@ function PlatformLayoutInner() { return ( -
-
+
+
diff --git a/frontend/src/app/platform/ProjectsPage.tsx b/frontend/src/app/platform/ProjectsPage.tsx index 902b89f..87e5f7e 100644 --- a/frontend/src/app/platform/ProjectsPage.tsx +++ b/frontend/src/app/platform/ProjectsPage.tsx @@ -58,6 +58,7 @@ import { } from './projectGraphStorage' import { toast } from 'sonner' import { NewProjectDialog } from './NewProjectDialog' +import { ProjectsPageBackground } from './ProjectsPageBackground' import type { Project } from './types' const PAGE_SIZE_OPTIONS = [10, 25, 50] as const @@ -315,7 +316,9 @@ export function ProjectsPage() { ) return ( -
+
+ +

@@ -797,6 +800,7 @@ export function ProjectsPage() { +

) } diff --git a/frontend/src/app/platform/ProjectsPageBackground.tsx b/frontend/src/app/platform/ProjectsPageBackground.tsx new file mode 100644 index 0000000..49e54db --- /dev/null +++ b/frontend/src/app/platform/ProjectsPageBackground.tsx @@ -0,0 +1,99 @@ +/** + * Subtle animated dot grid background for ProjectsPage. + * Matches canvas grid (20px gap), with gentle wave movement. + */ + +import React, { useCallback, useEffect, useRef } from 'react' + +const GAP = 20 +const SPEED = 0.010 +const AMPLITUDE = 4 +const DOT_SIZE = 1.2 +const PULSE_AMOUNT = 0.25 + +function getDotColor(): string { + if (typeof document === 'undefined') return 'rgba(128,128,128,0.25)' + const isDark = document.documentElement.classList.contains('dark') + return isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)' +} + +export function ProjectsPageBackground({ className }: { className?: string }) { + const canvasRef = useRef(null) + const timeRef = useRef(0) + const rafRef = useRef(0) + + const draw = useCallback(() => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext('2d') + if (!ctx) return + + const { width, height } = canvas + const cols = Math.floor(width / GAP) + 2 + const rows = Math.floor(height / GAP) + 2 + const t = timeRef.current + + ctx.fillStyle = 'transparent' + ctx.clearRect(0, 0, width, height) + + const dotColor = getDotColor() + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const baseX = col * GAP + const baseY = row * GAP + + const w1 = Math.sin(t * SPEED + baseX * 0.008 + baseY * 0.006) * AMPLITUDE + const w2 = Math.sin(t * SPEED * 0.7 - baseX * 0.012 + baseY * 0.01) * AMPLITUDE * 0.5 + const w3 = Math.sin(t * SPEED * 1.2 + baseY * 0.015) * AMPLITUDE * 0.3 + + const x = baseX + w1 + w2 + w3 + const y = baseY + w1 * 0.6 + w3 * 0.4 + + const size = DOT_SIZE + Math.sin(t * SPEED * 2 + baseX * 0.02) * PULSE_AMOUNT + + ctx.beginPath() + ctx.arc(x, y, size, 0, Math.PI * 2) + ctx.fillStyle = dotColor + ctx.fill() + } + } + + timeRef.current += 1 + rafRef.current = requestAnimationFrame(draw) + }, []) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + + const resize = () => { + const dpr = Math.min(window.devicePixelRatio ?? 1, 2) + const rect = canvas.getBoundingClientRect() + canvas.width = Math.round(rect.width * dpr) + canvas.height = Math.round(rect.height * dpr) + canvas.style.width = `${rect.width}px` + canvas.style.height = `${rect.height}px` + const ctx = canvas.getContext('2d') + if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + } + + resize() + window.addEventListener('resize', resize) + rafRef.current = requestAnimationFrame(draw) + return () => { + window.removeEventListener('resize', resize) + if (rafRef.current) cancelAnimationFrame(rafRef.current) + } + }, [draw]) + + return ( + + ) +}