From 8b527e062f24a8cafa83f648c9eee5acf550057d Mon Sep 17 00:00:00 2001 From: chait Date: Tue, 15 Sep 2026 06:21:03 -0400 Subject: [PATCH] added loading screen --- package-lock.json | 165 +++++++++++++++++++++++++++++++++++++- package.json | 3 +- src/components/Loader.css | 139 ++++++++++++++++++++++++++++++++ src/components/Loader.jsx | 116 +++++++++++++++++++++++++++ src/main.jsx | 2 + src/shaders/loader.wgsl | 46 +++++++++++ tests/site.spec.js | 10 +++ 7 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 src/components/Loader.css create mode 100644 src/components/Loader.jsx create mode 100644 src/shaders/loader.wgsl diff --git a/package-lock.json b/package-lock.json index a60108d..9f8ff2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.6.0" + "react-router-dom": "^7.6.0", + "vgpu": "^0.5.0" }, "devDependencies": { "@playwright/test": "^1.52.0", @@ -792,6 +793,31 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1276,6 +1302,58 @@ "dev": true, "license": "MIT" }, + "node_modules/@vgpu/adapter-mock": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@vgpu/adapter-mock/-/adapter-mock-0.5.0.tgz", + "integrity": "sha512-YtgXOFn1qI+8oTJenG2wWbQPA28V0CfE5UJs5Xqbt6qyR/xaZ5LHH2XWTwFcjnqn2b1xQCyTatJCMyyOdweUnw==", + "license": "MIT", + "dependencies": { + "@vgpu/core": "0.5.0" + } + }, + "node_modules/@vgpu/adapter-node": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@vgpu/adapter-node/-/adapter-node-0.5.0.tgz", + "integrity": "sha512-lgzqFcLDejwRgwlaNStzkwLNU9b4aN2OoPpWqT1UyPmK521BSw2OIQKzAuDIkzNlREla6KNC12G6bthhOzZKzw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@vgpu/core": "0.5.0", + "webgpu": "0.4.0" + } + }, + "node_modules/@vgpu/core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@vgpu/core/-/core-0.5.0.tgz", + "integrity": "sha512-z80DHRHViv6+FKKkPzggEtT1ZZs/WCKRQbzJuwYdA135DZaSbCMoRK4anhM+I/nOHuDpf7H3xjkwE6rfolWZUw==", + "license": "MIT", + "dependencies": { + "@vgpu/wgsl": "0.5.0" + } + }, + "node_modules/@vgpu/wgsl": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@vgpu/wgsl/-/wgsl-0.5.0.tgz", + "integrity": "sha512-PDlDJem7J28C9bk+vPy2kYZEoW8+nYIx2oN4nu7/WF2V7ETx32BtmWcawwMbhiu/CVsobFafML0h0eqLRBNa3Q==", + "license": "MIT", + "dependencies": { + "@vgpu/wgsl-std": "0.5.0" + }, + "peerDependencies": { + "@vgpu/adapter-node": ">=0.1.6 <1.0.0 || ^0.5.0-rc.0" + }, + "peerDependenciesMeta": { + "@vgpu/adapter-node": { + "optional": true + } + } + }, + "node_modules/@vgpu/wgsl-std": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@vgpu/wgsl-std/-/wgsl-std-0.5.0.tgz", + "integrity": "sha512-NjMyJxwy4evFNCSqFQy4EqAbZ7iU+NUyJYyG9tLUhbdDLzBmfZlk1S3PyRZ9olBFFfEiVW2JNVw10maPKLEs1A==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1297,6 +1375,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@webgpu/types": { + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz", + "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==", + "license": "BSD-3-Clause" + }, "node_modules/baseline-browser-mapping": { "version": "2.11.21", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", @@ -1389,7 +1473,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1552,7 +1635,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -1604,6 +1686,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pixelmatch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.2.0.tgz", + "integrity": "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==", + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/playwright": { "version": "1.63.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", @@ -1633,6 +1727,15 @@ "node": ">=20" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", @@ -1857,6 +1960,36 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/vgpu": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/vgpu/-/vgpu-0.5.0.tgz", + "integrity": "sha512-isQfMHRTysYFfT8UWTzqQI/ROw2L7lVkj7xoUJlvIF6FjYxBNS/YNG7qp3tZ7ZvZZ3uwYjso+geQv/bEq5Hl3w==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/server": "2.0.0", + "@vgpu/adapter-mock": "0.5.0", + "@vgpu/adapter-node": "0.5.0", + "@vgpu/core": "0.5.0", + "@vgpu/wgsl": "0.5.0", + "@vgpu/wgsl-std": "0.5.0", + "@webgpu/types": "^0.1.69", + "pixelmatch": "^7.2.0", + "pngjs": "^7.0.0", + "wgpu-matrix": "^3.4.2", + "zod": "^4.4.3" + }, + "bin": { + "vgpu": "bin/vgpu.js" + }, + "peerDependencies": { + "three": ">=0.180.0 <0.200.0" + }, + "peerDependenciesMeta": { + "three": { + "optional": true + } + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -1932,12 +2065,38 @@ } } }, + "node_modules/webgpu": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/webgpu/-/webgpu-0.4.0.tgz", + "integrity": "sha512-F5pimn3Aoi0zWjuRdiVs5TnrUwSzD2lESBohsIUsqyitWkGRQlXU2fhV6ycXlQTa1bvAf3sjqiUpBEpmSQ5ptA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@webgpu/types": "^0.1.69", + "debug": "^4.4.0" + } + }, + "node_modules/wgpu-matrix": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/wgpu-matrix/-/wgpu-matrix-3.4.2.tgz", + "integrity": "sha512-IfZFbG7olEYBD76VCZy2TzqM57GKy3LT++AZh37tMHPPByO6R2h61zXGBZFNPi2IzxNivNi8y5dvze5PjaczjA==", + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 66e673b..b483d54 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dependencies": { "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.6.0" + "react-router-dom": "^7.6.0", + "vgpu": "^0.5.0" }, "devDependencies": { "@playwright/test": "^1.52.0", diff --git a/src/components/Loader.css b/src/components/Loader.css new file mode 100644 index 0000000..5a19e12 --- /dev/null +++ b/src/components/Loader.css @@ -0,0 +1,139 @@ +.loader { + position: fixed; + inset: 0; + z-index: 1000; + display: grid; + grid-template-rows: auto 1fr auto; + overflow: hidden; + padding: clamp(24px, 3.5vw, 56px); + color: #f5f3ee; + background: #24231e; + transition: + opacity 650ms cubic-bezier(0.76, 0, 0.24, 1), + clip-path 650ms cubic-bezier(0.76, 0, 0.24, 1); + clip-path: inset(0); +} +.loader--closing { + opacity: 0; + clip-path: inset(0 0 100% 0); + pointer-events: none; +} +.loader__canvas, +.loader__veil { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} +.loader__canvas { + display: block; + opacity: 0; + transition: opacity 180ms ease-out; +} +.loader__canvas[data-ready="true"] { + opacity: 1; +} +.loader__veil { + background: linear-gradient(90deg, #17171238, transparent 45%, #17171238); +} +.loader__topline, +.loader__footer, +.loader__center { + position: relative; + z-index: 1; +} +.loader__topline, +.loader__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.16em; + text-transform: uppercase; +} +.loader__center { + align-self: center; + justify-self: center; + text-align: center; +} +.loader__eyebrow { + margin: 0 0 18px; + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.loader__title { + margin: 0; + font-family: var(--serif); + font-size: clamp(58px, 8.5vw, 132px); + font-weight: 400; + line-height: 0.84; + letter-spacing: -0.055em; +} +.loader__title em { + font-weight: 400; +} +.loader__footer { + display: grid; + grid-template-columns: auto 1fr auto; +} +.loader__track { + height: 1px; + overflow: hidden; + background: #ffffff38; +} +.loader__progress { + position: relative; + display: block; + width: 100%; + height: 100%; + background: #f5f3ee; + transform: translateX(-100%); + animation: loader-progress 1.9s linear forwards; +} +.loader__progress::after { + position: absolute; + top: 50%; + right: 0; + width: 7px; + height: 7px; + border: 1px solid #f5f3ee; + border-radius: 50%; + background: #29271f; + content: ""; + transform: translate(50%, -50%); +} +.loader__announcement { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +@keyframes loader-progress { + to { + transform: translateX(0); + } +} +@media (max-width: 600px) { + .loader__title { + font-size: clamp(54px, 19vw, 86px); + } + .loader__topline span:last-child { + display: none; + } +} +@media (prefers-reduced-motion: reduce) { + .loader { + transition: none; + } + .loader__progress { + animation: none; + } + .loader__progress { + transform: none; + } +} diff --git a/src/components/Loader.jsx b/src/components/Loader.jsx new file mode 100644 index 0000000..e0b84e9 --- /dev/null +++ b/src/components/Loader.jsx @@ -0,0 +1,116 @@ +import { useEffect, useRef, useState } from "react"; +import weaveShader from "../shaders/loader.wgsl?raw"; +import "./Loader.css"; + +export default function Loader() { + const canvasRef = useRef(null); + const [phase, setPhase] = useState("active"); + + useEffect(() => { + const reducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + const closeTimer = window.setTimeout( + () => setPhase("closing"), + reducedMotion ? 0 : 1900, + ); + const removeTimer = window.setTimeout( + () => setPhase("hidden"), + reducedMotion ? 50 : 2550, + ); + return () => { + window.clearTimeout(closeTimer); + window.clearTimeout(removeTimer); + }; + }, []); + + useEffect(() => { + if (phase !== "active") return undefined; + let active = true; + let gpu; + let loop; + let unsubscribeResize; + + async function renderWeave() { + try { + const { clock, effect, frameLoop, init, surface } = await import( + "vgpu" + ); + gpu = await init({ powerPreference: "low-power" }); + if (!active) return gpu.dispose(); + + const target = surface(gpu, canvasRef.current, { dpr: [1, 1.5] }); + const weave = effect(gpu, weaveShader, { + set: { params: { time: 0, progress: 0, aspect: 1, padding: 0 } }, + }); + const timer = clock(gpu); + unsubscribeResize = target.onResize(({ width, height }) => { + weave.set({ params: { aspect: width / height } }); + }); + let presented = false; + loop = frameLoop( + gpu, + (frame) => { + weave.set({ + params: { + time: timer.time, + progress: Math.min(1.2, timer.time / 1.8), + }, + }); + frame.pass(target, weave); + if (!presented) { + presented = true; + requestAnimationFrame(() => { + if (active) canvasRef.current.dataset.ready = "true"; + }); + } + }, + { fps: 30 }, + ); + } catch { + // A flat background remains when WebGPU is unavailable. + } + } + + if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) + renderWeave(); + + return () => { + active = false; + loop?.stop(); + unsubscribeResize?.(); + gpu?.dispose(); + }; + }, [phase]); + + if (phase === "hidden") return null; + + return ( +
+