feat: WebGL cinematic countdown — 3D atom with circular orbital text

- Three.js + React Three Fiber + postprocessing (bloom, chromatic aberration, glitch, scanlines)
- Nucleus with pulsing energy core, wireframe icosahedron shell
- 3 electron orbits with trail particles + energy arcs
- 600 particle cloud with organic movement
- Circular orbital text: title, countdown timer, subtitle curved on 3D rings
- Mouse interactions: camera parallax, atom speed boost, nucleus glow on proximity
- Camera smooth lerp follow with gentle auto-drift
- Font fix: woff2 → ttf for troika-three-text compatibility
- Logo: static mark instead of missing sprite frames

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clément SAILLANT
2026-03-29 17:32:38 +00:00
parent 666f05bf59
commit d5f1ce84b4
5 changed files with 1317 additions and 201 deletions
+665 -10
View File
@@ -11,13 +11,18 @@
"@astrojs/node": "^9.4.5",
"@astrojs/react": "^4.4.1",
"@radix-ui/react-slot": "^1.2.3",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@react-three/postprocessing": "^3.0.4",
"astro": "^5.13.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"motion": "^12.23.24",
"postprocessing": "^6.39.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"three": "^0.183.2"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
@@ -1013,9 +1018,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -1077,6 +1080,12 @@
"node": ">=18"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"license": "Apache-2.0"
},
"node_modules/@emmetio/abbreviation": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz",
@@ -2189,6 +2198,24 @@
"react": ">=16"
}
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.17",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
"license": "Apache-2.0"
},
"node_modules/@monogrid/gainmap-js": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
"integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
"license": "MIT",
"dependencies": {
"promise-worker-transferable": "^1.0.4"
},
"peerDependencies": {
"three": ">= 0.159.0"
}
},
"node_modules/@oslojs/encoding": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
@@ -2239,6 +2266,120 @@
}
}
},
"node_modules/@react-three/drei": {
"version": "10.7.7",
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
"integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mediapipe/tasks-vision": "0.10.17",
"@monogrid/gainmap-js": "^3.0.6",
"@use-gesture/react": "^10.3.1",
"camera-controls": "^3.1.0",
"cross-env": "^7.0.3",
"detect-gpu": "^5.0.56",
"glsl-noise": "^0.0.0",
"hls.js": "^1.5.17",
"maath": "^0.10.8",
"meshline": "^3.3.1",
"stats-gl": "^2.2.8",
"stats.js": "^0.17.0",
"suspend-react": "^0.1.3",
"three-mesh-bvh": "^0.8.3",
"three-stdlib": "^2.35.6",
"troika-three-text": "^0.52.4",
"tunnel-rat": "^0.1.2",
"use-sync-external-store": "^1.4.0",
"utility-types": "^3.11.0",
"zustand": "^5.0.1"
},
"peerDependencies": {
"@react-three/fiber": "^9.0.0",
"react": "^19",
"react-dom": "^19",
"three": ">=0.159"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/@react-three/fiber": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"its-fine": "^2.0.0",
"react-use-measure": "^2.1.7",
"scheduler": "^0.27.0",
"suspend-react": "^0.1.3",
"use-sync-external-store": "^1.4.0",
"zustand": "^5.0.3"
},
"peerDependencies": {
"expo": ">=43.0",
"expo-asset": ">=8.4",
"expo-file-system": ">=11.0",
"expo-gl": ">=11.0",
"react": ">=19 <19.3",
"react-dom": ">=19 <19.3",
"react-native": ">=0.78",
"three": ">=0.156"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"expo-asset": {
"optional": true
},
"expo-file-system": {
"optional": true
},
"expo-gl": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/@react-three/postprocessing": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz",
"integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==",
"license": "MIT",
"dependencies": {
"maath": "^0.6.0",
"n8ao": "^1.9.4",
"postprocessing": "^6.36.6"
},
"peerDependencies": {
"@react-three/fiber": "^9.0.0",
"react": "^19.0",
"three": ">= 0.156.0"
}
},
"node_modules/@react-three/postprocessing/node_modules/maath": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz",
"integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==",
"license": "MIT",
"peerDependencies": {
"@types/three": ">=0.144.0",
"three": ">=0.144.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -3230,6 +3371,12 @@
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"license": "MIT"
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -3313,6 +3460,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/draco3d": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3359,6 +3512,12 @@
"@types/unist": "*"
}
},
"node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -3379,6 +3538,15 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-reconciler": {
"version": "0.28.9",
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
@@ -3386,18 +3554,63 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.183.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": ">=0.5.17",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~1.0.1"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@use-gesture/core": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
"license": "MIT"
},
"node_modules/@use-gesture/react": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
"license": "MIT",
"dependencies": {
"@use-gesture/core": "10.3.1"
},
"peerDependencies": {
"react": ">= 16.8.0"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -3611,6 +3824,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@webgpu/types": {
"version": "0.1.69",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
"license": "BSD-3-Clause"
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -4472,6 +4691,26 @@
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
@@ -4497,6 +4736,15 @@
"node": ">=12.0.0"
}
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -4568,6 +4816,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/camelcase": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
@@ -4580,6 +4852,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camera-controls": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
"integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
"license": "MIT",
"engines": {
"node": ">=22.0.0",
"npm": ">=10.5.1"
},
"peerDependencies": {
"three": ">=0.126.1"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001775",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz",
@@ -4896,11 +5181,28 @@
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
"license": "MIT"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -5100,6 +5402,15 @@
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"license": "MIT"
},
"node_modules/detect-gpu": {
"version": "5.0.70",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
"license": "MIT",
"dependencies": {
"webgl-constants": "^1.1.1"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -5244,6 +5555,12 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/draco3d": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
"license": "Apache-2.0"
},
"node_modules/dset": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
@@ -5509,6 +5826,12 @@
}
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz",
@@ -5693,6 +6016,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glsl-noise": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
"license": "MIT"
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5907,6 +6236,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hls.js": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
"integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
"license": "Apache-2.0"
},
"node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -5949,6 +6284,32 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-meta-resolve": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
@@ -6054,6 +6415,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"license": "MIT"
},
"node_modules/is-wsl": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
@@ -6073,9 +6440,20 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/its-fine": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
"license": "MIT",
"dependencies": {
"@types/react-reconciler": "^0.28.9"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@@ -6167,6 +6545,15 @@
"node": ">=6"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
@@ -6470,6 +6857,16 @@
"lz-string": "bin/bin.js"
}
},
"node_modules/maath": {
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
"license": "MIT",
"peerDependencies": {
"@types/three": ">=0.134.0",
"three": ">=0.134.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -6731,6 +7128,21 @@
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"license": "CC0-1.0"
},
"node_modules/meshline": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
"license": "MIT",
"peerDependencies": {
"three": ">=0.137"
}
},
"node_modules/meshoptimizer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
"license": "MIT"
},
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -7428,6 +7840,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/n8ao": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz",
"integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==",
"license": "ISC",
"peerDependencies": {
"postprocessing": ">=6.30.0",
"three": ">=0.137"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -7739,7 +8161,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -7838,6 +8259,21 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postprocessing": {
"version": "6.39.0",
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz",
"integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==",
"license": "Zlib",
"peerDependencies": {
"three": ">= 0.168.0 < 0.184.0"
}
},
"node_modules/potpack": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
"license": "ISC"
},
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
@@ -7879,6 +8315,16 @@
"node": ">=6"
}
},
"node_modules/promise-worker-transferable": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
"license": "Apache-2.0",
"dependencies": {
"is-promise": "^2.1.0",
"lie": "^3.0.2"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -7987,6 +8433,21 @@
"node": ">=0.10.0"
}
},
"node_modules/react-use-measure": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.13",
"react-dom": ">=16.13"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
@@ -8231,7 +8692,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -8487,7 +8947,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -8500,7 +8959,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -8582,6 +9040,32 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stats-gl": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
"license": "MIT",
"dependencies": {
"@types/three": "*",
"three": "^0.170.0"
},
"peerDependencies": {
"@types/three": "*",
"three": "*"
}
},
"node_modules/stats-gl/node_modules/three": {
"version": "0.170.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
"license": "MIT"
},
"node_modules/stats.js": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
"license": "MIT"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -9268,6 +9752,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/suspend-react": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=17.0"
}
},
"node_modules/svgo": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
@@ -9324,6 +9817,44 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/three": {
"version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
"license": "MIT"
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
"integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
"license": "MIT",
"peerDependencies": {
"three": ">= 0.159.0"
}
},
"node_modules/three-stdlib": {
"version": "2.36.1",
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
"integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
"license": "MIT",
"dependencies": {
"@types/draco3d": "^1.4.0",
"@types/offscreencanvas": "^2019.6.4",
"@types/webxr": "^0.5.2",
"draco3d": "^1.4.1",
"fflate": "^0.6.9",
"potpack": "^1.0.1"
},
"peerDependencies": {
"three": ">=0.128.0"
}
},
"node_modules/three-stdlib/node_modules/fflate": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
"license": "MIT"
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -9401,6 +9932,36 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/troika-three-text": {
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
"license": "MIT",
"dependencies": {
"bidi-js": "^1.0.2",
"troika-three-utils": "^0.52.4",
"troika-worker-utils": "^0.52.0",
"webgl-sdf-generator": "1.1.1"
},
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/troika-three-utils": {
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
"license": "MIT",
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/troika-worker-utils": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
"license": "MIT"
},
"node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
@@ -9462,6 +10023,43 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tunnel-rat": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
"license": "MIT",
"dependencies": {
"zustand": "^4.3.2"
}
},
"node_modules/tunnel-rat/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
@@ -9850,6 +10448,24 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/utility-types": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -10264,6 +10880,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/webgl-constants": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
},
"node_modules/webgl-sdf-generator": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
"license": "MIT"
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
@@ -10275,7 +10902,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -10642,6 +11268,35 @@
"zod": "^3"
}
},
"node_modules/zustand": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+6 -1
View File
@@ -26,13 +26,18 @@
"@astrojs/node": "^9.4.5",
"@astrojs/react": "^4.4.1",
"@radix-ui/react-slot": "^1.2.3",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@react-three/postprocessing": "^3.0.4",
"astro": "^5.13.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"motion": "^12.23.24",
"postprocessing": "^6.39.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"three": "^0.183.2"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
Binary file not shown.
+526
View File
@@ -0,0 +1,526 @@
import { useRef, useMemo, useEffect, useState, useCallback } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { Text } from '@react-three/drei';
import { EffectComposer, Bloom, ChromaticAberration, Scanline, Glitch as GlitchEffect } from '@react-three/postprocessing';
import { GlitchMode, BlendFunction } from 'postprocessing';
import * as THREE from 'three';
/* ===================================================================
ATOM GLITCH — cinematic WebGL countdown for L'Electron Rare
- Nucleus core with pulsing energy
- 3 electron orbits with trail particles
- Particle cloud (electrons as sparks)
- Post-processing: bloom, chromatic aberration, glitch, scanlines
- Mouse-reactive + auto-orbit
=================================================================== */
const COLORS = {
cyan: new THREE.Color('#5bd1d8'),
amber: new THREE.Color('#f1c27a'),
green: new THREE.Color('#b6d18f'),
electric: new THREE.Color('#0071e3'),
white: new THREE.Color('#ffffff'),
nucleus: new THREE.Color('#ff6b35'),
};
/* ---------- Particle cloud around the atom ---------- */
function ParticleField({ count = 600 }) {
const ref = useRef<THREE.Points>(null);
const { positions, velocities, colors } = useMemo(() => {
const pos = new Float32Array(count * 3);
const vel = new Float32Array(count * 3);
const col = new Float32Array(count * 3);
const palette = [COLORS.cyan, COLORS.amber, COLORS.green, COLORS.electric];
for (let i = 0; i < count; i++) {
const r = 2 + Math.random() * 6;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
pos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
pos[i * 3 + 2] = r * Math.cos(phi);
vel[i * 3] = (Math.random() - 0.5) * 0.004;
vel[i * 3 + 1] = (Math.random() - 0.5) * 0.004;
vel[i * 3 + 2] = (Math.random() - 0.5) * 0.004;
const c = palette[Math.floor(Math.random() * palette.length)];
col[i * 3] = c.r;
col[i * 3 + 1] = c.g;
col[i * 3 + 2] = c.b;
}
return { positions: pos, velocities: vel, colors: col };
}, [count]);
useFrame(({ clock }) => {
if (!ref.current) return;
const posAttr = ref.current.geometry.attributes.position as THREE.BufferAttribute;
const arr = posAttr.array as Float32Array;
const t = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
arr[i * 3] += velocities[i * 3] + Math.sin(t * 0.5 + i) * 0.002;
arr[i * 3 + 1] += velocities[i * 3 + 1] + Math.cos(t * 0.3 + i) * 0.002;
arr[i * 3 + 2] += velocities[i * 3 + 2];
// respawn far particles
const dist = Math.sqrt(arr[i * 3] ** 2 + arr[i * 3 + 1] ** 2 + arr[i * 3 + 2] ** 2);
if (dist > 8) {
const r = 2 + Math.random() * 2;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
arr[i * 3] = r * Math.sin(phi) * Math.cos(theta);
arr[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
arr[i * 3 + 2] = r * Math.cos(phi);
}
}
posAttr.needsUpdate = true;
});
return (
<points ref={ref}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
<bufferAttribute attach="attributes-color" args={[colors, 3]} />
</bufferGeometry>
<pointsMaterial size={0.04} vertexColors transparent opacity={0.7} sizeAttenuation depthWrite={false} blending={THREE.AdditiveBlending} />
</points>
);
}
/* ---------- Orbit trail (instanced tube of spheres) ---------- */
function OrbitTrail({ tilt, speed, color, trailCount = 30 }: { tilt: number[]; speed: number; color: THREE.Color; trailCount?: number }) {
const meshRef = useRef<THREE.InstancedMesh>(null);
const phase = useMemo(() => Math.random() * Math.PI * 2, []);
const dummy = useMemo(() => new THREE.Object3D(), []);
const orbitA = 3.2;
const orbitB = orbitA * 0.38;
useFrame(({ clock }) => {
if (!meshRef.current) return;
const t = clock.getElapsedTime() * speed + phase;
for (let i = 0; i < trailCount; i++) {
const age = i / trailCount;
const angle = t - age * 0.8;
dummy.position.set(Math.cos(angle) * orbitA, Math.sin(angle) * orbitB, 0);
const s = (1 - age) * 0.14 + 0.02;
dummy.scale.setScalar(s);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<group rotation={tilt as [number, number, number]}>
<instancedMesh ref={meshRef} args={[undefined, undefined, trailCount]}>
<sphereGeometry args={[1, 8, 8]} />
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={2} transparent opacity={0.9} />
</instancedMesh>
</group>
);
}
/* ---------- Orbit ring (thin glowing line) ---------- */
function OrbitRing({ tilt, color }: { tilt: number[]; color: THREE.Color }) {
const points = useMemo(() => {
const pts: THREE.Vector3[] = [];
for (let i = 0; i <= 128; i++) {
const t = (i / 128) * Math.PI * 2;
pts.push(new THREE.Vector3(Math.cos(t) * 3.2, Math.sin(t) * 3.2 * 0.38, 0));
}
return pts;
}, []);
const geo = useMemo(() => new THREE.BufferGeometry().setFromPoints(points), [points]);
return (
<group rotation={tilt as [number, number, number]}>
<line geometry={geo}>
<lineBasicMaterial color={color} transparent opacity={0.12} />
</line>
</group>
);
}
/* ---------- Nucleus: pulsing energy core ---------- */
function Nucleus() {
const coreRef = useRef<THREE.Mesh>(null);
const glowRef = useRef<THREE.Mesh>(null);
const outerRef = useRef<THREE.Mesh>(null);
useFrame(({ clock, pointer }) => {
const t = clock.getElapsedTime();
const mouseDist = Math.sqrt(pointer.x ** 2 + pointer.y ** 2);
const mouseProximity = Math.max(0, 1 - mouseDist); // 1 at center, 0 at edges
if (coreRef.current) {
const s = 1 + Math.sin(t * 3) * 0.08 + Math.sin(t * 7.3) * 0.03 + mouseProximity * 0.15;
coreRef.current.scale.setScalar(s);
(coreRef.current.material as THREE.MeshStandardMaterial).emissiveIntensity = 2 + mouseProximity * 3;
}
if (glowRef.current) {
const s = 1 + Math.sin(t * 2) * 0.15 + mouseProximity * 0.3;
glowRef.current.scale.setScalar(s);
(glowRef.current.material as THREE.MeshStandardMaterial).opacity = 0.15 + Math.sin(t * 4) * 0.08 + mouseProximity * 0.2;
}
if (outerRef.current) {
outerRef.current.rotation.y = t * (0.5 + mouseProximity * 2);
outerRef.current.rotation.x = t * (0.3 + mouseProximity * 1.5);
}
});
return (
<group>
{/* outer energy shell */}
<mesh ref={outerRef}>
<icosahedronGeometry args={[0.9, 1]} />
<meshStandardMaterial color={COLORS.electric} wireframe transparent opacity={0.08} emissive={COLORS.electric} emissiveIntensity={0.5} />
</mesh>
{/* glow sphere */}
<mesh ref={glowRef}>
<sphereGeometry args={[0.7, 32, 32]} />
<meshStandardMaterial color={COLORS.nucleus} transparent opacity={0.15} emissive={COLORS.nucleus} emissiveIntensity={1.2} />
</mesh>
{/* solid core */}
<mesh ref={coreRef}>
<sphereGeometry args={[0.32, 32, 32]} />
<meshStandardMaterial color="#ffffff" emissive={COLORS.amber} emissiveIntensity={2} metalness={0.6} roughness={0.2} />
</mesh>
</group>
);
}
/* ---------- Energy arcs (random lightning) ---------- */
function EnergyArc({ color }: { color: THREE.Color }) {
const ref = useRef<THREE.Line>(null);
const [visible, setVisible] = useState(true);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
// flash randomly
setVisible(Math.sin(t * 12 + Math.random() * 100) > 0.7);
if (ref.current && visible) {
const geo = ref.current.geometry;
const pts: THREE.Vector3[] = [];
const segments = 8;
const startAngle = Math.sin(t * 2) * Math.PI;
for (let i = 0; i <= segments; i++) {
const frac = i / segments;
const r = 0.4 + frac * 2.5;
const angle = startAngle + frac * 1.5;
pts.push(new THREE.Vector3(
Math.cos(angle) * r + (Math.random() - 0.5) * 0.3,
Math.sin(angle) * r * 0.4 + (Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.4,
));
}
geo.setFromPoints(pts);
}
});
if (!visible) return null;
return (
<line ref={ref}>
<bufferGeometry />
<lineBasicMaterial color={color} transparent opacity={0.4} blending={THREE.AdditiveBlending} />
</line>
);
}
/* ---------- 3D Circular / Orbital text ---------- */
const LAUNCH = new Date('2026-05-01T00:00:00+02:00').getTime();
const FONT_URL = '/assets/fonts/manrope-regular.ttf';
function pad(n: number) { return String(n).padStart(2, '0'); }
/* Each character placed on a circle arc, rotating together */
function CircularText({ text, radius, speed, tilt, fontSize, color, emissive, opacity = 0.9, spread = 0.8 }: {
text: string; radius: number; speed: number; tilt: number[];
fontSize: number; color: string; emissive: string; opacity?: number; spread?: number;
}) {
const groupRef = useRef<THREE.Group>(null);
const chars = useMemo(() => text.split(''), [text]);
const charWidth = fontSize * 0.55;
useFrame(({ clock }) => {
if (!groupRef.current) return;
const t = clock.getElapsedTime() * speed;
groupRef.current.rotation.y = t;
});
return (
<group rotation={tilt as [number, number, number]}>
<group ref={groupRef}>
{chars.map((char, i) => {
const angle = ((i - chars.length / 2) * charWidth * spread) / radius;
const x = Math.sin(angle) * radius;
const z = Math.cos(angle) * radius;
return (
<Text
key={i}
position={[x, 0, z]}
rotation={[0, -angle, 0]}
fontSize={fontSize}
font={FONT_URL}
anchorX="center"
anchorY="middle"
>
{char}
<meshStandardMaterial
color={color}
emissive={emissive}
emissiveIntensity={0.6}
transparent
opacity={opacity}
side={THREE.DoubleSide}
/>
</Text>
);
})}
</group>
</group>
);
}
/* Countdown — characters on a spinning ring */
function CountdownRing() {
const [time, setTime] = useState('');
const groupRef = useRef<THREE.Group>(null);
const radius = 4.2;
useEffect(() => {
function tick() {
const diff = Math.max(0, LAUNCH - Date.now());
const d = Math.floor(diff / 86400000);
const h = Math.floor((diff % 86400000) / 3600000);
const m = Math.floor((diff % 3600000) / 60000);
const s = Math.floor((diff % 60000) / 1000);
setTime(`${pad(d)}J ${pad(h)}H ${pad(m)}M ${pad(s)}S`);
}
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
useFrame(({ clock }) => {
if (!groupRef.current) return;
groupRef.current.rotation.y = clock.getElapsedTime() * -0.2;
});
const chars = time.split('');
const fontSize = 0.28;
const charWidth = fontSize * 0.5;
return (
<group rotation={[0.3, 0, 0.1]}>
<group ref={groupRef}>
{chars.map((char, i) => {
const angle = ((i - chars.length / 2) * charWidth * 0.9) / radius;
const x = Math.sin(angle) * radius;
const z = Math.cos(angle) * radius;
return (
<Text
key={i}
position={[x, 0, z]}
rotation={[0, -angle, 0]}
fontSize={fontSize}
font={FONT_URL}
anchorX="center"
anchorY="middle"
>
{char}
<meshStandardMaterial
color="#ffffff"
emissive="#5bd1d8"
emissiveIntensity={0.8}
transparent
opacity={0.95}
side={THREE.DoubleSide}
/>
</Text>
);
})}
</group>
</group>
);
}
/* All circular texts assembled */
function CountdownText() {
return (
<group>
{/* "L'electron" — large ring, slow spin */}
<CircularText
text={"L'\u00e9lectron"}
radius={3.8}
speed={0.15}
tilt={[-0.3, 0, 0]}
fontSize={0.55}
color="#ffffff"
emissive="#ffffff"
spread={0.9}
/>
{/* "rare" — opposite tilt, bigger */}
<CircularText
text="rare"
radius={3.5}
speed={-0.2}
tilt={[0.5, 0.2, 0]}
fontSize={0.85}
color="#ffffff"
emissive="#5bd1d8"
spread={1.0}
/>
{/* Subtitle — wider ring, slow */}
<CircularText
text="SYSTEMES ELECTRONIQUES SPECIFIQUES"
radius={5.2}
speed={0.08}
tilt={[1.3, 0.2, 0.4]}
fontSize={0.11}
color="#ffffff"
emissive="#5bd1d8"
opacity={0.35}
spread={0.7}
/>
{/* "LANCEMENT DANS" — small ring */}
<CircularText
text="LANCEMENT DANS"
radius={3.2}
speed={0.25}
tilt={[-0.7, -0.3, 0.2]}
fontSize={0.1}
color="#5bd1d8"
emissive="#5bd1d8"
opacity={0.5}
spread={0.8}
/>
{/* Countdown timer — spinning ring */}
<CountdownRing />
{/* Extra decorative text rings */}
<CircularText
text="electronique automatisme energie stockage prototypage formation"
radius={5.8}
speed={-0.05}
tilt={[0.8, -0.4, 0.6]}
fontSize={0.07}
color="#5bd1d8"
emissive="#5bd1d8"
opacity={0.2}
spread={0.5}
/>
</group>
);
}
/* ---------- Main scene ---------- */
function AtomScene() {
const groupRef = useRef<THREE.Group>(null);
const orbits = useMemo(() => [
{ tilt: [-0.5, 0, 0], speed: 1.0, color: COLORS.cyan },
{ tilt: [0.5, 0.3, 0], speed: 0.72, color: COLORS.amber },
{ tilt: [1.57, 0.2, 0.4], speed: 0.55, color: COLORS.green },
], []);
useFrame(({ clock, pointer }) => {
if (!groupRef.current) return;
const t = clock.getElapsedTime();
// Mouse distance from center = speed boost
const mouseDist = Math.sqrt(pointer.x ** 2 + pointer.y ** 2);
const speedMul = 1 + mouseDist * 0.8;
groupRef.current.rotation.y = t * 0.12 * speedMul + pointer.x * 0.6;
groupRef.current.rotation.x = -0.2 + pointer.y * 0.4;
groupRef.current.rotation.z = pointer.x * 0.1;
});
return (
<group ref={groupRef}>
<Nucleus />
{orbits.map((o, i) => (
<OrbitRing key={`ring-${i}`} tilt={o.tilt} color={o.color} />
))}
{orbits.map((o, i) => (
<OrbitTrail key={`trail-${i}`} tilt={o.tilt} speed={o.speed} color={o.color} />
))}
<EnergyArc color={COLORS.cyan} />
<EnergyArc color={COLORS.amber} />
<ParticleField />
{/* Textes orbitants — dans le groupe, tournent avec l'atome */}
<CountdownText />
</group>
);
}
/* ---------- Camera with mouse parallax ---------- */
function CameraRig() {
const { camera } = useThree();
const mouse = useRef({ x: 0, y: 0 });
const smooth = useRef({ x: 0, y: 0 });
useFrame(({ clock, pointer }) => {
const t = clock.getElapsedTime();
// Smooth mouse follow (lerp)
mouse.current.x = pointer.x;
mouse.current.y = pointer.y;
smooth.current.x += (mouse.current.x - smooth.current.x) * 0.05;
smooth.current.y += (mouse.current.y - smooth.current.y) * 0.05;
// Camera orbits gently + follows mouse
camera.position.x = Math.sin(t * 0.08) * 0.5 + smooth.current.x * 2.5;
camera.position.y = Math.cos(t * 0.06) * 0.3 + smooth.current.y * 1.5;
camera.position.z = 9 + Math.sin(t * 0.1) * 0.3;
camera.lookAt(smooth.current.x * 0.5, smooth.current.y * 0.3, 0);
});
return null;
}
/* ---------- Exported component ---------- */
export default function AtomGlitch() {
return (
<div
style={{
width: '100vw',
height: '100vh',
}}
aria-label="Atome 3D animé — L'Electron Rare"
role="img"
>
<Canvas
camera={{ position: [0, -0.5, 9], fov: 45 }}
dpr={[1, 2]}
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance' }}
style={{ background: 'transparent' }}
>
<color attach="background" args={['#000000']} />
<fog attach="fog" args={['#000000', 8, 18]} />
<ambientLight intensity={0.15} />
<pointLight position={[5, 3, 5]} intensity={0.8} color="#ffffff" />
<pointLight position={[-4, -2, 3]} intensity={0.4} color="#5bd1d8" />
<pointLight position={[0, 4, -3]} intensity={0.3} color="#f1c27a" />
<CameraRig />
<AtomScene />
<EffectComposer>
<Bloom luminanceThreshold={0.2} luminanceSmoothing={0.9} intensity={1.8} mipmapBlur />
<ChromaticAberration blendFunction={BlendFunction.NORMAL} offset={new THREE.Vector2(0.002, 0.002)} />
<GlitchEffect delay={new THREE.Vector2(3, 8)} duration={new THREE.Vector2(0.1, 0.4)} strength={new THREE.Vector2(0.05, 0.15)} mode={GlitchMode.SPORADIC} />
<Scanline blendFunction={BlendFunction.OVERLAY} density={1.8} opacity={0.05} />
</EffectComposer>
</Canvas>
</div>
);
}
+120 -190
View File
@@ -1,63 +1,42 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import AtomGlitch from '@/components/AtomGlitch';
import '@/styles/global.css';
---
<BaseLayout title="L'Electron Rare — Bientôt" description="L'Electron Rare arrive. Systèmes électroniques spécifiques, formations, prototypage.">
<main class="countdown-page">
<div class="logo-sprite" id="logo-sprite" aria-label="L'Electron Rare — logo animé"></div>
<svg class="atom" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-cyan-soft, #5bd1d866)" stroke-width="0.8" transform="rotate(-30 150 150)"/>
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-amber-soft, #f1c27a66)" stroke-width="0.8" transform="rotate(30 150 150)"/>
<ellipse cx="150" cy="150" rx="120" ry="45" fill="none" stroke="var(--trace-green-soft, #b6d18f44)" stroke-width="0.8" transform="rotate(90 150 150)"/>
<circle cx="150" cy="150" r="18" fill="url(#nucleusGlow)"/>
<circle cx="150" cy="150" r="8" fill="var(--accent, #f1c27a)"/>
<circle r="5" fill="var(--electric, #5bd1d8)"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion></circle>
<circle r="10" fill="var(--electric, #5bd1d8)" opacity="0.3"><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#o1"/></animateMotion></circle>
<circle r="4" fill="var(--accent, #f1c27a)"><animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion></circle>
<circle r="9" fill="var(--accent, #f1c27a)" opacity="0.25"><animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#o2"/></animateMotion></circle>
<circle r="3" fill="var(--trace-green, #b6d18f)"><animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion></circle>
<circle r="8" fill="var(--trace-green, #b6d18f)" opacity="0.2"><animateMotion dur="5.8s" repeatCount="indefinite"><mpath href="#o3"/></animateMotion></circle>
<defs>
<ellipse id="o1" cx="150" cy="150" rx="120" ry="45" transform="rotate(-30 150 150)" fill="none"/>
<ellipse id="o2" cx="150" cy="150" rx="120" ry="45" transform="rotate(30 150 150)" fill="none"/>
<ellipse id="o3" cx="150" cy="150" rx="120" ry="45" transform="rotate(90 150 150)" fill="none"/>
<radialGradient id="nucleusGlow">
<stop offset="0%" stop-color="var(--accent, #f1c27a)" stop-opacity="0.6"/>
<stop offset="100%" stop-color="var(--accent, #f1c27a)" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
<div class="title-bubble">
<h1 class="title" data-text="L'électron rare">L'<span class="electric">é</span>lectron rare</h1>
<!-- WebGL background -->
<div class="webgl-bg">
<AtomGlitch client:load />
</div>
<div class="timer-bubble">
<div class="timer" id="timer">
<div class="unit"><span class="num" id="days">--</span><span class="label">jours</span></div>
<div class="sep">:</div>
<div class="unit"><span class="num" id="hours">--</span><span class="label">heures</span></div>
<div class="sep">:</div>
<div class="unit"><span class="num" id="minutes">--</span><span class="label">minutes</span></div>
<div class="sep">:</div>
<div class="unit"><span class="num" id="seconds">--</span><span class="label">secondes</span></div>
<!-- Overlay content -->
<div class="overlay">
<!-- Top bar -->
<div class="top-bar">
<div class="logo-sprite" id="logo-sprite" aria-label="L'Electron Rare — logo animé"></div>
<span class="brand-mark">L'ELECTRON RARE</span>
</div>
</div>
<div class="tagline-bubble" id="tagline-bubble" aria-live="polite">
<span class="tagline-text" id="tagline"></span>
</div>
<!-- Center — spacer (text is in WebGL) -->
<div class="center-spacer">
<div class="tagline-area" id="tagline-bubble" aria-live="polite">
<span class="tagline-cursor">></span>
<span class="tagline-text" id="tagline"></span>
</div>
</div>
<div class="footnote-bubble">
<span>Systèmes électroniques spécifiques · Formations · Prototypage</span>
<!-- Bottom bar -->
<div class="bottom-bar">
<span class="bottom-left">electronique · automatisme · energie</span>
<span class="bottom-right">mai 2026</span>
</div>
</div>
</main>
</BaseLayout>
<script>
const LAUNCH = new Date('2026-05-01T00:00:00+02:00').getTime();
const messages = [
"Charge des condensateurs en cours...",
"Calibration de l'oscilloscope...",
@@ -77,17 +56,9 @@ import '@/styles/global.css';
];
let msgIndex = Math.floor(Math.random() * messages.length);
function pad(n) { return String(n).padStart(2, '0'); }
function update() {
const diff = Math.max(0, LAUNCH - Date.now());
document.getElementById('days').textContent = pad(Math.floor(diff / 86400000));
document.getElementById('hours').textContent = pad(Math.floor((diff % 86400000) / 3600000));
document.getElementById('minutes').textContent = pad(Math.floor((diff % 3600000) / 60000));
document.getElementById('seconds').textContent = pad(Math.floor((diff % 60000) / 1000));
}
function rotateMessage() {
const bubble = document.getElementById('tagline-bubble');
const el = document.getElementById('tagline');
const bubble = document.getElementById('tagline-bubble')!;
const el = document.getElementById('tagline')!;
bubble.style.opacity = '0';
bubble.style.transform = 'translateY(4px)';
setTimeout(() => {
@@ -98,180 +69,139 @@ import '@/styles/global.css';
}, 400);
}
// Logo sprite animation - cycle through 4 frames
const frames = [
'/assets/brand/logo-frame-1.png',
'/assets/brand/logo-frame-2.png',
'/assets/brand/logo-frame-3.png',
'/assets/brand/logo-frame-4.png',
'/assets/brand/logo-frame-3.png',
'/assets/brand/logo-frame-2.png',
];
let frameIdx = 0;
const sprite = document.getElementById('logo-sprite');
const sprite = document.getElementById('logo-sprite')!;
sprite.style.backgroundImage = `url(/assets/brand/logo-mark.png)`;
// Preload frames
frames.forEach(f => { const img = new Image(); img.src = f; });
function animateLogo() {
sprite.style.opacity = '0.85';
setTimeout(() => {
sprite.style.backgroundImage = `url(${frames[frameIdx]})`;
sprite.style.opacity = '1';
frameIdx = (frameIdx + 1) % frames.length;
}, 150);
}
update(); rotateMessage();
sprite.style.backgroundImage = `url(${frames[0]})`;
setInterval(update, 1000);
rotateMessage();
setInterval(rotateMessage, 8000);
setInterval(animateLogo, 2000);
</script>
<style>
.countdown-page {
position: relative;
min-height: 100vh;
overflow: hidden;
background: #000;
font-family: 'Manrope', -apple-system, 'SF Pro Display', 'Helvetica Neue', sans-serif;
color: #fff;
}
/* WebGL canvas — full background */
.webgl-bg {
position: fixed;
inset: 0;
z-index: 0;
width: 100vw;
height: 100vh;
}
/* Overlay — full viewport, vertical layout */
.overlay {
position: relative;
z-index: 10;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: clamp(32px, 6vw, 80px) clamp(20px, 4vw, 48px);
background-color: #ffffff;
font-family: 'Manrope', -apple-system, 'SF Pro Display', 'Helvetica Neue', sans-serif;
justify-content: space-between;
padding: clamp(20px, 3vw, 40px) clamp(24px, 5vw, 60px);
pointer-events: none;
}
.overlay > * { pointer-events: auto; }
/* Logo — clean, static display */
/* ===== TOP BAR ===== */
.top-bar {
display: flex;
align-items: center;
gap: 16px;
animation: fade-in 0.8s ease both;
}
.logo-sprite {
width: clamp(120px, 18vw, 180px);
height: clamp(120px, 18vw, 180px);
width: 48px;
height: 48px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-bottom: clamp(16px, 3vw, 32px);
transition: opacity 0.3s ease;
animation: gentle-float 6s ease-in-out infinite;
filter: brightness(1.2) drop-shadow(0 0 12px rgba(91, 209, 216, 0.4));
}
@keyframes gentle-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
.brand-mark {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.25em;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
}
.atom {
width: clamp(100px, 16vw, 150px);
height: auto;
margin-bottom: clamp(16px, 2vw, 24px);
opacity: 0.12;
}
/* Title bubble */
.title-bubble {
margin-bottom: clamp(24px, 4vw, 40px);
padding: 0;
animation: fade-in 0.8s ease both;
}
.title {
font-family: 'Manrope', -apple-system, 'SF Pro Display', sans-serif;
font-size: clamp(32px, 6vw, 56px);
font-weight: 800;
color: #1d1d1f;
margin: 0;
letter-spacing: -0.03em;
line-height: 1.1;
}
.electric {
color: #0071e3;
}
/* Timer */
.timer-bubble {
background: #f5f5f7;
border: none;
border-radius: 20px;
padding: clamp(16px, 3vw, 28px) clamp(24px, 4vw, 48px);
margin-bottom: clamp(20px, 3vw, 32px);
animation: fade-in 0.8s 0.2s ease both;
}
.timer {
display: flex;
align-items: center;
gap: clamp(8px, 2vw, 16px);
}
.unit {
/* ===== CENTER SPACER ===== */
.center-spacer {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.num {
font-family: 'Manrope', -apple-system, 'SF Pro Display', sans-serif;
font-size: clamp(36px, 8vw, 72px);
font-weight: 300;
color: #1d1d1f;
line-height: 1;
min-width: 2ch;
font-variant-numeric: tabular-nums;
}
.label {
font-family: 'Manrope', -apple-system, 'SF Pro Display', sans-serif;
font-size: clamp(9px, 1.2vw, 12px);
color: #86868b;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-top: 8px;
font-weight: 500;
}
.sep {
font-family: 'Manrope', -apple-system, 'SF Pro Display', sans-serif;
font-size: clamp(24px, 5vw, 48px);
color: #d2d2d7;
font-weight: 200;
margin-bottom: 18px;
justify-content: flex-end;
padding-bottom: clamp(20px, 3vw, 40px);
}
/* Tagline */
.tagline-bubble {
padding: 12px 24px;
margin-bottom: clamp(16px, 2vw, 24px);
min-height: 44px;
/* Tagline — terminal style */
.tagline-area {
display: flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
transition: opacity 0.4s ease, transform 0.4s ease;
animation: fade-in 0.8s 0.4s ease both;
animation: fade-in 0.8s 1.1s ease both;
}
.tagline-cursor {
font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
font-size: 14px;
color: #5bd1d8;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
.tagline-text {
font-family: 'Manrope', -apple-system, 'SF Pro Display', sans-serif;
font-size: clamp(13px, 1.6vw, 16px);
color: #86868b;
font-weight: 400;
letter-spacing: 0.01em;
}
/* Footnote */
.footnote-bubble {
padding: clamp(8px, 1.5vw, 12px) 20px;
animation: fade-in 0.8s 0.6s ease both;
}
.footnote-bubble span {
font-family: 'Manrope', -apple-system, 'SF Pro Display', sans-serif;
font-size: clamp(11px, 1.2vw, 13px);
color: #aeaeb2;
letter-spacing: 0.02em;
font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
font-size: clamp(11px, 1.3vw, 14px);
color: rgba(255, 255, 255, 0.4);
font-weight: 400;
}
/* Animations */
/* ===== BOTTOM BAR ===== */
.bottom-bar {
display: flex;
justify-content: space-between;
align-items: flex-end;
animation: fade-in 0.8s 1.2s ease both;
}
.bottom-left, .bottom-right {
font-size: clamp(10px, 1.1vw, 12px);
font-weight: 400;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.15);
text-transform: uppercase;
}
/* ===== Animations ===== */
@keyframes fade-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 640px) {
.timer-hero { gap: 2px; }
.sep { margin: 0; }
.bottom-bar { flex-direction: column; align-items: center; gap: 4px; }
}
@media (prefers-reduced-motion: reduce) {
.logo-sprite { animation: none; }
.title-bubble, .timer-bubble, .tagline-bubble, .footnote-bubble {
animation: none;
opacity: 1;
}
.logo-sprite, .title, .timer-hero, .divider,
.tagline-area, .subtitle, .overline,
.top-bar, .bottom-bar { animation: none; opacity: 1; }
.webgl-bg { display: none; }
}
</style>