diff --git a/package-lock.json b/package-lock.json index 5b13faf..1dda233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,26 @@ "name": "doormile-next", "version": "0.1.0", "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", + "@react-three/postprocessing": "^3.0.4", + "framer-motion": "^12.40.0", "gsap": "^3.15.0", + "lenis": "^1.3.23", "next": "16.2.6", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "three": "^0.171.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.171.0", "eslint": "^9", "eslint-config-next": "16.2.6", + "puppeteer-core": "^23.11.1", "tailwindcss": "^4", "typescript": "^5" } @@ -229,6 +237,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -1036,6 +1053,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "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/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -1247,6 +1282,156 @@ "node": ">=12.4.0" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.6.1", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz", + "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==", + "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/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1534,6 +1719,19 @@ "tailwindcss": "4.3.0" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "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/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1545,6 +1743,12 @@ "tslib": "^2.4.0" } }, + "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.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1576,11 +1780,16 @@ "undici-types": "~6.21.0" } }, + "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.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1596,6 +1805,52 @@ "@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/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.171.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.171.0.tgz", + "integrity": "sha512-oLuT1SAsT+CUg/wxUTFHo0K3NtJLnx9sJhZWQJp/0uXqFpzSk1hRHmvWvpaAWSfvx2db0lVKZ5/wV0I0isD2mQ==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "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/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.60.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", @@ -2204,6 +2459,30 @@ "win32" ] }, + "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/@webgpu/types": { + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz", + "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", + "license": "BSD-3-Clause" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2227,6 +2506,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2244,6 +2533,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2437,6 +2736,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2490,6 +2802,21 @@ "node": ">= 0.4" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2497,6 +2824,123 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.3.tgz", + "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "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.32", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", @@ -2509,6 +2953,25 @@ "node": ">=6.0.0" } }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.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/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -2567,6 +3030,40 @@ "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/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2627,6 +3124,19 @@ "node": ">=6" } }, + "node_modules/camera-controls": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz", + "integrity": "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==", + "license": "MIT", + "engines": { + "node": ">=20.11.0", + "npm": ">=10.8.2" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -2664,12 +3174,51 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2704,11 +3253,28 @@ "dev": true, "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", @@ -2723,7 +3289,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2733,6 +3298,16 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2848,6 +3423,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "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", @@ -2858,6 +3457,13 @@ "node": ">=8" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2871,6 +3477,12 @@ "node": ">=0.10.0" } }, + "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/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2900,6 +3512,16 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", @@ -3114,6 +3736,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", @@ -3474,6 +4118,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3520,6 +4178,37 @@ "node": ">=0.10.0" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3527,6 +4216,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -3581,6 +4277,22 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3661,6 +4373,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3722,6 +4461,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3761,6 +4510,22 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -3792,6 +4557,21 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3835,6 +4615,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3972,6 +4758,60 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3982,6 +4822,12 @@ "node": ">= 4" } }, + "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-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4024,6 +4870,16 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4208,6 +5064,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4294,6 +5160,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4450,7 +5322,6 @@ "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/iterator.prototype": { @@ -4471,6 +5342,18 @@ "node": ">= 0.4" } }, + "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/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4594,6 +5477,37 @@ "node": ">=0.10" } }, + "node_modules/lenis": { + "version": "1.3.23", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz", + "integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4608,6 +5522,15 @@ "node": ">= 0.8.0" } }, + "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.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -4915,6 +5838,16 @@ "yallist": "^3.0.2" } }, + "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", @@ -4945,6 +5878,21 @@ "node": ">= 8" } }, + "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": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4982,6 +5930,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4989,6 +5959,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.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -5030,6 +6010,16 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", @@ -5263,6 +6253,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5331,6 +6331,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5358,7 +6392,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" @@ -5371,6 +6404,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5429,6 +6469,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postprocessing": { + "version": "6.39.1", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.1.tgz", + "integrity": "sha512-R2dG2zy+BAx3USl5EHw+PvnrlbT5PKnZVp3se0HCR0pWH8WQdh742yNG4YWOsq6c0bFpffk0Gd2RqPeoP/wKng==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.168.0 < 0.185.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/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5439,6 +6494,26 @@ "node": ">= 0.8.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5451,6 +6526,54 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5461,6 +6584,24 @@ "node": ">=6" } }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5510,6 +6651,21 @@ "dev": true, "license": "MIT" }, + "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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5554,6 +6710,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5815,7 +6990,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" @@ -5828,7 +7002,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" @@ -5910,6 +7083,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5926,6 +7151,32 @@ "dev": true, "license": "MIT" }, + "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/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5940,6 +7191,40 @@ "node": ">= 0.4" } }, + "node_modules/streamx": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.26.0.tgz", + "integrity": "sha512-VvNG1K72Po/xwJzxZFnZ++Tbrv4lwSptsbkFuzXCJAYZvCK5nnxsvXU6ajqkv7chyiI1Y0YXq2Jh8Iy8Y7NF/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6053,6 +7338,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6125,6 +7423,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/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", @@ -6146,6 +7453,99 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/three": { + "version": "0.171.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.171.0.tgz", + "integrity": "sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==", + "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/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6207,6 +7607,36 @@ "node": ">=8.0" } }, + "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/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6252,6 +7682,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-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6343,6 +7810,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6400,6 +7874,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "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.1.13" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -6486,11 +7996,39 @@ "punycode": "^2.1.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/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/which": { "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" @@ -6601,6 +8139,63 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6608,6 +8203,46 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6643,6 +8278,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "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 + } + } } } } diff --git a/package.json b/package.json index a20b1ee..366566c 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,26 @@ "lint": "eslint" }, "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", + "@react-three/postprocessing": "^3.0.4", + "framer-motion": "^12.40.0", "gsap": "^3.15.0", + "lenis": "^1.3.23", "next": "16.2.6", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "three": "^0.171.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.171.0", "eslint": "^9", "eslint-config-next": "16.2.6", + "puppeteer-core": "^23.11.1", "tailwindcss": "^4", "typescript": "^5" } diff --git a/public/images/home-bg-1.png b/public/images/home-bg-1.png new file mode 100644 index 0000000..1e7768c Binary files /dev/null and b/public/images/home-bg-1.png differ diff --git a/public/images/truck.png b/public/images/truck.png new file mode 100644 index 0000000..e887162 Binary files /dev/null and b/public/images/truck.png differ diff --git a/src/app/miletruth/page.tsx b/src/app/miletruth/page.tsx index d9718e9..b302009 100644 --- a/src/app/miletruth/page.tsx +++ b/src/app/miletruth/page.tsx @@ -3,6 +3,8 @@ import MileTruthHero from "../../components/sections/MileTruthHero"; import Workflow1 from "../../components/sections/Workflow1"; import Workflow2 from "../../components/sections/Workflow2"; import Workflow3 from "../../components/sections/Workflow3"; +import OptimizationSection from "../../components/optimization/OptimizationSection"; +import PerformanceSection from "../../components/performance/PerformanceSection"; export const metadata = { title: "MileTruth – Doormile", @@ -16,9 +18,11 @@ export default function MileTruthPage() {
+ +
diff --git a/src/components/optimization/AICore.tsx b/src/components/optimization/AICore.tsx new file mode 100644 index 0000000..4aa25f5 --- /dev/null +++ b/src/components/optimization/AICore.tsx @@ -0,0 +1,158 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; +import { COLORS } from "./constants"; +import { seeded, smoothstep } from "./math"; + +type Props = { + progress: React.RefObject; + reduced?: boolean; +}; + +const CORE_Y = 3.4; + +type CalcBeamProps = { + offset: THREE.Vector3; + index: number; + progress: React.RefObject; + color: string; +}; + +function CalculationBeam({ offset, index, progress, color }: CalcBeamProps) { + const lineRef = useRef(null); + const lineMatRef = useRef(null); + const packetRefs = useRef<(THREE.Mesh | null)[]>([]); + + const packetCount = 3; + const offsets = useMemo(() => Array.from({ length: packetCount }, (_, k) => k / packetCount), []); + + useFrame((state) => { + const p = progress.current ?? 0; + const t = state.clock.elapsedTime; + + // Beams are active during AI Scan and Route Generation phases (progress 0.22 -> 0.76) + const activeFactor = smoothstep(0.20, 0.40, p) * (1 - smoothstep(0.72, 0.85, p)); + const isVisible = activeFactor > 0.01; + + if (lineRef.current) lineRef.current.visible = isVisible; + if (lineMatRef.current && isVisible) { + lineMatRef.current.opacity = activeFactor * (0.15 + Math.sin(t * 12 + index) * 0.08); + } + + packetRefs.current.forEach((mesh, k) => { + if (!mesh) return; + mesh.visible = isVisible; + if (!isVisible) return; + + const speed = 0.8 + seeded(index * 5 + k) * 0.4; + const u = (offsets[k] + t * (speed / 3)) % 1; + + // Linearly interpolate from center [0,0,0] to hub offset + mesh.position.copy(offset).multiplyScalar(u); + + const mat = mesh.material as THREE.MeshBasicMaterial; + mat.opacity = activeFactor * Math.sin(u * Math.PI) * 0.8; + + const s = 0.04 + Math.sin(t * 8 + k) * 0.015; + mesh.scale.setScalar(s / 0.06); + }); + }); + + // Pre-generated geometry for the static connecting line + const lineGeo = useMemo(() => { + const geo = new THREE.BufferGeometry(); + const pts = new Float32Array([0, 0, 0, offset.x, offset.y, offset.z]); + geo.setAttribute("position", new THREE.BufferAttribute(pts, 3)); + return geo; + }, [offset]); + + return ( + + {/* Connector line segment */} + + + + + {/* Streams of flowing energy data packets */} + {Array.from({ length: packetCount }).map((_, k) => ( + { + packetRefs.current[k] = el; + }} + visible={false} + > + + + + ))} + + ); +} + +const CalculationBeamMemo = React.memo(CalculationBeam); + +/** + * The Doormile AI core: a clean floating holographic orb with energy rings, + * a particle shell and neural links. It powers up during the AI-scan phase, + * fires an expanding radar scan, then a route-optimization burst. + */ +function AICore({ progress, reduced = false }: Props) { + const root = useRef(null); + + const hubOffsets = useMemo(() => { + return Array.from({ length: 5 }, (_, c) => { + const baseAngle = (c / 5) * Math.PI * 2 + 0.3; + const radius = 7 + seeded(c * 7 + 1) * 3; + const cx = Math.cos(baseAngle) * radius; + const cz = Math.sin(baseAngle) * radius; + return new THREE.Vector3(cx, 0.1 - CORE_Y, cz); + }); + }, []); + + useFrame((state) => { + const t = state.clock.elapsedTime; + + if (root.current) { + root.current.position.y = CORE_Y + Math.sin(t * 0.8) * 0.12; + } + }); + + return ( + + {/* Sleek, subtle dispatch beacon directly above warehouse tower */} + + + + + + {/* Route Calculation/Analysis Beams */} + {hubOffsets.map((offset, i) => ( + + ))} + + ); +} + +export default React.memo(AICore); diff --git a/src/components/optimization/HologramCity.tsx b/src/components/optimization/HologramCity.tsx new file mode 100644 index 0000000..cb24050 --- /dev/null +++ b/src/components/optimization/HologramCity.tsx @@ -0,0 +1,191 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; +import { COLORS } from "./constants"; +import { damp, lerp, seeded, smoothstep } from "./math"; + +type Props = { + progress: React.RefObject; + count?: number; + reduced?: boolean; +}; + +const CYAN = new THREE.Color(COLORS.cyan); +const RED = new THREE.Color(COLORS.red); +const GREEN = new THREE.Color(COLORS.green); + +// Generate deterministic cluster centers matching routes.ts +const clusterCount = 5; +const clusterCenters = Array.from({ length: clusterCount }, (_, c) => { + const baseAngle = (c / clusterCount) * Math.PI * 2 + 0.3; + const radius = 7 + seeded(c * 7 + 1) * 3; + const cx = Math.cos(baseAngle) * radius; + const cz = Math.sin(baseAngle) * radius; + return new THREE.Vector3(cx, 0.1, cz); +}); + +function HologramCity({ progress, reduced = false }: Props) { + const mainWarehouseRef = useRef(null); + const radarRefs = useRef<(THREE.Group | null)[]>([]); + const corridorMats = useRef<(THREE.LineBasicMaterial | null)[]>([]); + const zoneMats = useRef<(THREE.MeshBasicMaterial | null)[]>([]); + const eased = useRef(0); + + // Pre-generate corridor geometries (Warehouse [0, 0, 0] to Cluster Centers) + const corridorGeometries = useMemo(() => { + return clusterCenters.map((center) => { + const pts = [ + new THREE.Vector3(0, 0.15, 0), + new THREE.Vector3(center.x * 0.5, 0.6, center.z * 0.5), // arch up slightly in the middle + new THREE.Vector3(center.x, 0.15, center.z), + ]; + return new THREE.CatmullRomCurve3(pts).getPoints(24); + }); + }, []); + + useFrame((state, dt) => { + const p = progress.current ?? 0; + eased.current = damp(eased.current, p, 3, dt); + const e = eased.current; + const t = state.clock.elapsedTime; + + // Animate Main Warehouse central beacon rotation + if (mainWarehouseRef.current) { + mainWarehouseRef.current.position.y = Math.sin(t * 1.2) * 0.04; + } + + // Rotate Radar dishes on top of the Regional Hubs + radarRefs.current.forEach((radar, idx) => { + if (radar) { + radar.rotation.y = t * (0.8 + idx * 0.2); + } + }); + + // Animate corridor line opacity and color: + // Under unoptimized states (chaos/scan) they are faint or red. + // As optimization settles, they light up with active cyan/green. + corridorMats.current.forEach((mat, idx) => { + if (mat) { + const activeColor = idx % 2 === 0 ? CYAN : GREEN; + const colorFactor = smoothstep(0.45, 0.8, e); + mat.color.copy(RED).lerp(activeColor, colorFactor); + mat.opacity = lerp(0.12, 0.7, colorFactor) * (0.8 + Math.sin(t * 4 - idx * 0.5) * 0.2); + } + }); + + // Animate ground delivery zone circles + zoneMats.current.forEach((mat, idx) => { + if (mat) { + const activeColor = idx % 2 === 0 ? CYAN : GREEN; + const colorFactor = smoothstep(0.5, 0.82, e); + mat.color.copy(RED).lerp(activeColor, colorFactor); + mat.opacity = lerp(0.06, 0.22, colorFactor) * (0.85 + Math.sin(t * 3 - idx) * 0.15); + } + }); + }); + + return ( + + {/* Sleek Dark Cyber Ground Grid */} + + + + + {/* Primary Transport Corridors (Warehouse to Cluster Hubs) */} + {corridorGeometries.map((points, i) => ( + + + [p.x, p.y, p.z])), 3]} + /> + + { + corridorMats.current[i] = el; + }} + transparent + opacity={0.15} + depthWrite={false} + /> + + ))} + + {/* CENTRAL LOGISTICS ASSET: Main Warehouse Hub */} + + {/* Core foundation structure */} + + + + + + + + + + {/* Loading Docks */} + {[-0.6, 0, 0.6].map((offset, i) => ( + + + + + ))} + + {/* Upper Level / Control Deck */} + + + + + + + + + + {/* Rooftop Solar Panels */} + {[-0.4, 0.4].map((offset, i) => ( + + + + + ))} + + {/* Central Communications Array / Tower */} + + {/* Main rod */} + + + + + {/* Glowing Beacon */} + + + + + + + {/* Pulsing ground base glow */} + + + + + + + ); +} + +export default React.memo(HologramCity); diff --git a/src/components/optimization/MetricsPanel.tsx b/src/components/optimization/MetricsPanel.tsx new file mode 100644 index 0000000..8c64bd5 --- /dev/null +++ b/src/components/optimization/MetricsPanel.tsx @@ -0,0 +1,152 @@ +"use client"; + +import React from "react"; +import { motion, useTransform, type MotionValue } from "framer-motion"; +import { COLORS, KPIS, Kpi, rgba } from "./constants"; + +type Props = { + /** shared scroll progress (0 → 1) */ + scroll: MotionValue; +}; + +const COUNT_START = 0.7; +const COUNT_END = 0.97; + +function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue; index: number }) { + const [jitter, setJitter] = React.useState(0); + + React.useEffect(() => { + const interval = setInterval(() => { + // Tiny micro-fluctuation between -0.4% and +0.4% + const val = (Math.random() - 0.5) * 0.008; + setJitter(val); + }, 800 + Math.random() * 600); // slightly staggered updates + return () => clearInterval(interval); + }, []); + + // Single reactive string — updates without re-rendering React. + const value = useTransform(scroll, (v) => { + const t = Math.min(1, Math.max(0, (v - COUNT_START) / (COUNT_END - COUNT_START))); + let n = kpi.before + (kpi.after - kpi.before) * t; + + // Apply high-frequency live operational fluctuations on settled state + if (t > 0.95) { + n = n * (1 + jitter); + } + + const afterSide = t > 0.5; + const prefix = afterSide ? kpi.prefixAfter ?? "" : kpi.prefixBefore ?? ""; + + if (kpi.key === "delayed" && kpi.after === 0 && t > 0.95) { + return `0`; + } + + return `${prefix}${Math.round(n)}${kpi.suffix ?? ""}`; + }); + + const label = useTransform(scroll, (v) => + v > (COUNT_START + COUNT_END) / 2 ? kpi.labelAfter : kpi.labelBefore, + ); + + const fromColor = kpi.key === "orders" ? COLORS.cyan : COLORS.red; + const toColor = COLORS.green; + const accent = useTransform( + scroll, + [COUNT_START, COUNT_END], + [rgba(fromColor, 1), rgba(toColor, 1)], + ); + const glow = useTransform( + scroll, + [COUNT_START, COUNT_END], + [rgba(fromColor, 0.28), rgba(toColor, 0.32)], + ); + const boxShadow = useTransform(glow, (g) => `0 10px 40px -12px ${g}, inset 0 1px 0 rgba(255,255,255,0.06)`); + const borderColor = useTransform( + scroll, + [COUNT_START, COUNT_END], + [rgba(fromColor, 0.4), rgba(toColor, 0.45)], + ); + + // Improvement arrow appears once optimized. + const arrowOpacity = useTransform(scroll, [COUNT_END - 0.12, COUNT_END], [0, 1]); + // Bar fills as the counter morphs from before → after. + const fillScale = useTransform(scroll, [COUNT_START, COUNT_END], [0.12, 1]); + + const trendColor = kpi.goodWhenLower ? COLORS.green : (kpi.key === "orders" ? COLORS.cyan : COLORS.green); + + const sparklinePath = React.useMemo(() => { + switch (kpi.key) { + case "distance": + case "vehicles": + return "M 0,3 C 16,3 32,17 64,17"; // Downward trend + case "orders": + return "M 0,17 C 16,6 32,19 64,8"; // Stable high wavy trend + case "delayed": + case "cost": + return "M 0,1 C 16,1 28,19 64,19"; // Sharp drop + default: + return "M 0,10 L 64,10"; + } + }, [kpi.key]); + + return ( + +
+ {label} + + {kpi.goodWhenLower ? "▼" : "▲"} + +
+ + {value} + + + {/* Sparkline trend graphic */} +
+ + + + + + + + + + +
+ +
+ +
+
+ ); +} + +const MetricCard = React.memo(Metric); + +function MetricsPanel({ scroll }: Props) { + return ( +
+ {KPIS.map((kpi, i) => ( + + ))} +
+ ); +} + +export default React.memo(MetricsPanel); diff --git a/src/components/optimization/OptimizationCanvas.tsx b/src/components/optimization/OptimizationCanvas.tsx new file mode 100644 index 0000000..c654582 --- /dev/null +++ b/src/components/optimization/OptimizationCanvas.tsx @@ -0,0 +1,85 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { Canvas, useFrame } from "@react-three/fiber"; +import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing"; +import { KernelSize } from "postprocessing"; +import * as THREE from "three"; +import { COLORS } from "./constants"; +import { damp, lerp, seeded } from "./math"; +import HologramCity from "./HologramCity"; +import RouteSystem from "./RouteSystem"; +import VehicleFleet from "./VehicleFleet"; +import AICore from "./AICore"; + +type Props = { + progress: React.RefObject; + reduced?: boolean; + isMobile?: boolean; + /** Pause the render loop when the section is scrolled off-screen. */ + active?: boolean; +}; + +/** Slow cinematic camera move from a high chaotic view to a settled framing. */ +function CameraRig({ progress }: { progress: React.RefObject }) { + const eased = useRef(0); + useFrame((state, dt) => { + const p = progress.current ?? 0; + eased.current = damp(eased.current, p, 1.5, dt); + const e = eased.current; + const t = state.clock.elapsedTime; + + const radius = lerp(17, 13, e); + const angle = lerp(-0.5, 0.45, e) + t * 0.02; + const height = lerp(9, 6.5, e) + Math.sin(t * 0.4) * 0.3; + + const cam = state.camera; + cam.position.x = Math.sin(angle) * radius; + cam.position.z = Math.cos(angle) * radius; + cam.position.y = height; + cam.lookAt(0, 2.4, 0); + }); + return null; +} + +function OptimizationCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) { + const cityCount = isMobile ? 48 : 90; + + return ( + + + + + + + + + + + + {/* Bloom is what turns the additive-blended wireframes/beams into a real + glowing hologram. Skipped under reduced-motion; lighter on mobile. */} + {!reduced && ( + + + + + )} + + ); +} + +export default React.memo(OptimizationCanvas); diff --git a/src/components/optimization/OptimizationSection.tsx b/src/components/optimization/OptimizationSection.tsx new file mode 100644 index 0000000..94150c1 --- /dev/null +++ b/src/components/optimization/OptimizationSection.tsx @@ -0,0 +1,881 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import dynamic from "next/dynamic"; +import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import Lenis from "lenis"; +import { + COLORS, + PHASE_LABELS, + PhaseKey, + phaseFromProgress, + rgba, +} from "./constants"; +import MetricsPanel from "./MetricsPanel"; + +// 3D scene is client-only and code-split so it never blocks first paint. +const OptimizationCanvas = dynamic(() => import("./OptimizationCanvas"), { + ssr: false, +}); + +const PHASE_ORDER: PhaseKey[] = [ + "chaos", + "scan", + "dissolve", + "optimize", + "reorganize", + "metrics", +]; + +const WORKFLOW_STEPS = [ + { label: "Analyze", icon: "🔍", activateAt: 0 }, + { label: "Optimize", icon: "⚡", activateAt: 2 }, + { label: "Assign", icon: "🚛", activateAt: 3 }, + { label: "Execute", icon: "📡", activateAt: 4 }, + { label: "Monitor", icon: "📊", activateAt: 5 }, +]; + +export default function OptimizationSection() { + const containerRef = useRef(null); + const progressRef = useRef(0); + const scroll = useMotionValue(0); + + const [phase, setPhase] = useState("chaos"); + const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before"); + const [mountScene, setMountScene] = useState(false); + const [sceneActive, setSceneActive] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [reduced, setReduced] = useState(false); + + const [orders, setOrders] = useState(59); + const [accuracy, setAccuracy] = useState(98.7); + const [activeVehicles, setActiveVehicles] = useState(5); + const [carbon, setCarbon] = useState(-12.0); + const [routeHealth, setRouteHealth] = useState(99.4); + + // Interval timers for high-fidelity live dashboard fluctuations + useEffect(() => { + const ordersInterval = setInterval(() => { + setOrders((prev) => prev + (Math.random() > 0.4 ? 1 : 0)); + }, 4500); + + const accuracyInterval = setInterval(() => { + setAccuracy((prev) => { + const delta = (Math.random() - 0.5) * 0.15; + const next = prev + delta; + return parseFloat(Math.min(99.1, Math.max(98.4, next)).toFixed(2)); + }); + }, 2800); + + const vehicleInterval = setInterval(() => { + setActiveVehicles((prev) => (prev === 5 ? (Math.random() > 0.5 ? 4 : 5) : (Math.random() > 0.3 ? 5 : 4))); + }, 3500); + + const carbonInterval = setInterval(() => { + setCarbon((prev) => { + const delta = (Math.random() - 0.5) * 0.2; + const next = prev + delta; + return parseFloat(Math.min(-11.5, Math.max(-12.8, next)).toFixed(1)); + }); + }, 3200); + + const healthInterval = setInterval(() => { + setRouteHealth((prev) => { + const delta = (Math.random() - 0.5) * 0.12; + const next = prev + delta; + return parseFloat(Math.min(99.9, Math.max(98.8, next)).toFixed(2)); + }); + }, 2500); + + return () => { + clearInterval(ordersInterval); + clearInterval(accuracyInterval); + clearInterval(vehicleInterval); + clearInterval(carbonInterval); + clearInterval(healthInterval); + }; + }, []); + + // Environment detection (client only). + useEffect(() => { + const mqMobile = window.matchMedia("(max-width: 767px)"); + const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)"); + const sync = () => { + setIsMobile(mqMobile.matches); + setReduced(mqReduce.matches); + }; + sync(); + mqMobile.addEventListener("change", sync); + mqReduce.addEventListener("change", sync); + return () => { + mqMobile.removeEventListener("change", sync); + mqReduce.removeEventListener("change", sync); + }; + }, []); + + // Mount the WebGL scene when the section approaches the viewport, and pause + // its render loop entirely once it scrolls off-screen (battery / 60fps). + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const mountIo = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + setMountScene(true); + // Activate immediately on mount so the render loop starts even on a + // direct scroll-jump (the activeIo below only fires on later changes). + setSceneActive(true); + mountIo.disconnect(); + } + }, + { rootMargin: "120% 0px" }, + ); + const activeIo = new IntersectionObserver( + (entries) => setSceneActive(entries.some((e) => e.isIntersecting)), + { rootMargin: "10% 0px" }, + ); + mountIo.observe(el); + activeIo.observe(el); + return () => { + mountIo.disconnect(); + activeIo.disconnect(); + }; + }, []); + + // Drive the shared scroll progress with GSAP ScrollTrigger. + // We pin via our own `position: fixed` (toggled by `pinState`) rather than + // GSAP's pin or CSS `position: sticky`. CSS sticky is broken by an ancestor + // (.body-container) using `overflow: hidden`, and GSAP's pin offsets the + // element by the fixed site-header height. A self-managed fixed element + // pins to the viewport top deterministically and ignores ancestor overflow. + useEffect(() => { + const el = containerRef.current; + if (!el) return; + gsap.registerPlugin(ScrollTrigger); + + // Smooth scroll (Lenis), driven by a SINGLE rAF source — GSAP's ticker. + // Previously lenis.raf() was called from both a manual requestAnimationFrame + // loop AND gsap.ticker, double-stepping the integrator every frame, which is + // what made scrolling stutter. One source keeps Lenis + ScrollTrigger locked. + const lenis = new Lenis({ + duration: 1.05, + easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), + orientation: "vertical", + gestureOrientation: "vertical", + smoothWheel: true, + }); + + lenis.on("scroll", ScrollTrigger.update); + const tickerCb = (time: number) => lenis.raf(time * 1000); // ticker is in seconds, Lenis wants ms + gsap.ticker.add(tickerCb); + gsap.ticker.lagSmoothing(0); + + let lastPhase: PhaseKey = "chaos"; + let lastPin: "before" | "pinned" | "after" = "before"; + const st = ScrollTrigger.create({ + trigger: el, + start: "top top", + end: "bottom bottom", + scrub: 0.4, + invalidateOnRefresh: true, + onUpdate: (self) => { + const p = self.progress; + progressRef.current = p; + scroll.set(p); + const next = phaseFromProgress(p); + if (next !== lastPhase) { + lastPhase = next; + setPhase(next); + } + const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned"; + if (ns !== lastPin) { + lastPin = ns; + setPinState(ns); + } + }, + }); + + const refresh = setTimeout(() => ScrollTrigger.refresh(), 300); + return () => { + clearTimeout(refresh); + st.kill(); + gsap.ticker.remove(tickerCb); + lenis.destroy(); + }; + }, [scroll]); + + // Overlay reactions to scroll (no React re-render — direct DOM updates). + const leftOpacity = useTransform(scroll, [0.3, 0.55], [1, 0.32]); + const leftBlur = useTransform(scroll, [0.3, 0.55], [0, 3]); + const leftFilter = useTransform(leftBlur, (b) => `blur(${b}px)`); + const rightOpacity = useTransform(scroll, [0.42, 0.66], [0.36, 1]); + const scanWidth = useTransform(scroll, [0, 1], ["0%", "100%"]); + const scanLineY = useTransform(scroll, [0.2, 0.42], ["8%", "92%"]); + const scanLineOpacity = useTransform(scroll, [0.18, 0.22, 0.42, 0.46], [0, 1, 1, 0]); + const dividerOpacity = useTransform(scroll, [0.45, 0.6], [0.15, 0.75]); + + const phaseIndex = PHASE_ORDER.indexOf(phase); + + return ( +
+
+
+ {/* Static backdrop (also the canvas loading state) */} +
+ + {/* 3D scene */} + {mountScene && ( +
+ +
+ )} + + {/* Depth vignette */} +
+ + {/* Floating AI Status Badge replacing giant abstract sphere centerpiece */} + + {phase !== "chaos" && ( + +
+ + + {PHASE_LABELS[phase]} + +
+
+ )} +
+ + {/* Center scan divider */} + + + {/* AI scan sweep line */} + + + {/* UI overlay */} +
+ {/* Header */} +
+ + Doormile AI Control Tower + + + AI Logistics Optimization Engine + + + Watch Doormile's AI engine transform chaotic logistics into precision-optimized delivery networks — reducing distance, fleet size, delays, and cost in real time. + + + {/* Workflow step indicators */} + + {WORKFLOW_STEPS.map((step, i) => { + const isActive = phaseIndex >= step.activateAt; + const isCurrent = phaseIndex === step.activateAt; + return ( + + {i > 0 && } + + {step.icon} + {step.label} + + + ); + })} + + + {/* Scan progress bar */} +
+
+ +
+
+
+ + {/* Side comparison panels */} +
+ +
+ System: Congested +
+

Without Optimization

+
    +
  • Chaotic overlapping routes
  • +
  • Duplicate & idle trips
  • +
  • 8 vehicles required
  • +
  • 23 delivery delays
  • +
  • +18% cost overrun
  • +
+
+ + +
+ System: Optimized +
+

With Doormile AI

+
    +
  • Optimized route clusters
  • +
  • Intelligent vehicle assignment
  • +
  • Multi-trip & EV planning
  • +
  • Zero delivery delays
  • +
  • 18% cost saved
  • +
  • Carbon footprint reduced
  • +
+
+
+ + {/* KPI metrics */} +
+ + {/* Bottom insight bar */} + + + Live Analytics: {orders} Orders + + AI Accuracy: {accuracy}% + + Fleet: {activeVehicles}/5 EV Active + + Route Health: {routeHealth}% + + Carbon: {carbon}% + +
+
+
+
+ + +
+ ); +} + +const styles = ` +/* ===== OUTER SECTION: Transparent so the card floats ===== */ +.dm-opt { + position: relative; + height: 230vh; + background: transparent; + margin-bottom: 120px; +} +.dm-opt-sticky { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + overflow: hidden; + background: transparent; +} +.dm-opt.is-pinned .dm-opt-sticky { position: fixed; top: 0; left: 0; } +.dm-opt.is-after .dm-opt-sticky { position: absolute; top: auto; bottom: 0; } + +/* ===== FLOATING CARD — the only colored surface ===== */ +.dm-opt-card { + position: absolute !important; + top: 110px !important; + left: 40px !important; + right: 40px !important; + bottom: 24px !important; + border-radius: 60px !important; + overflow: hidden !important; + background: linear-gradient(165deg, #06101f 0%, #020617 35%, #040d1c 70%, #030a18 100%) !important; + border: 1.5px solid ${rgba("#ffffff", 0.08)} !important; + box-shadow: + 0 0 0 1px ${rgba(COLORS.cyan, 0.04)}, + 0 4px 30px -4px rgba(0, 0, 0, 0.7), + 0 20px 80px -20px rgba(0, 0, 0, 0.6), + 0 0 120px -30px ${rgba(COLORS.cyan, 0.08)}, + inset 0 1px 0 ${rgba("#ffffff", 0.06)}, + inset 0 -1px 0 ${rgba("#ffffff", 0.02)} !important; + box-sizing: border-box !important; +} +/* Animated subtle grid pattern */ +.dm-opt-card::before { + content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; + opacity: 0.035; + background-image: + linear-gradient(${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px), + linear-gradient(90deg, ${rgba(COLORS.cyan, 0.5)} 1px, transparent 1px); + background-size: 60px 60px; + animation: dmOptGridDrift 25s linear infinite; +} +@keyframes dmOptGridDrift { + 0% { background-position: 0 0; } + 100% { background-position: 60px 60px; } +} +/* Radial center glow behind 3D scene */ +.dm-opt-card::after { + content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; + background: + radial-gradient(ellipse 50% 45% at 50% 48%, ${rgba(COLORS.cyan, 0.08)} 0%, transparent 70%), + radial-gradient(ellipse 60% 40% at 50% 90%, ${rgba(COLORS.green, 0.05)} 0%, transparent 60%), + radial-gradient(ellipse 80% 50% at 50% 10%, ${rgba(COLORS.cyan, 0.04)} 0%, transparent 50%); +} + +@media (max-width: 1024px) { + .dm-opt-card { + top: 96px !important; + left: 20px !important; + right: 20px !important; + bottom: 16px !important; + border-radius: 42px !important; + } +} +@media (max-width: 767px) { + .dm-opt-card { + top: 86px !important; + left: 10px !important; + right: 10px !important; + bottom: 10px !important; + border-radius: 28px !important; + } +} + +/* ===== INNER LAYERS ===== */ +.dm-opt-backdrop { + position: absolute; + inset: 0; z-index: 0; + background: + radial-gradient(100% 70% at 50% 6%, ${rgba(COLORS.cyan, 0.06)} 0%, transparent 55%), + radial-gradient(80% 60% at 50% 100%, ${rgba(COLORS.green, 0.05)} 0%, transparent 55%); +} +.dm-opt-canvas { position: absolute; inset: 0; z-index: 1; } +.dm-opt-canvas canvas { display: block; } +.dm-opt-vignette { + position: absolute; inset: 0; z-index: 2; pointer-events: none; + background: + radial-gradient(110% 90% at 50% 50%, transparent 48%, ${rgba("#020617", 0.88)} 100%), + linear-gradient(180deg, ${rgba("#020617", 0.6)} 0%, transparent 20%, transparent 65%, ${rgba("#020617", 0.92)} 100%); +} +.dm-opt-divider { + position: absolute; left: 50%; top: 14%; bottom: 28%; + width: 1px; z-index: 3; pointer-events: none; transform: translateX(-0.5px); + background: linear-gradient(180deg, transparent, ${rgba(COLORS.cyan, 0.6)}, transparent); + box-shadow: 0 0 16px ${rgba(COLORS.cyan, 0.4)}; +} +.dm-opt-scanline { + position: absolute; left: 6%; right: 6%; height: 2px; z-index: 3; pointer-events: none; + background: linear-gradient(90deg, transparent, ${COLORS.cyan}, transparent); + box-shadow: 0 0 22px ${rgba(COLORS.cyan, 0.8)}; +} + +/* ===== FLOATING AI STATUS BADGE ===== */ +.dm-opt-floating-badge { + pointer-events: none; +} +.dm-opt-floating-badge__inner { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 18px; + border-radius: 999px; + background: ${rgba(COLORS.ink, 0.85)}; + border: 1.5px solid ${rgba(COLORS.cyan, 0.3)}; + box-shadow: + 0 10px 30px -5px rgba(0, 0, 0, 0.65), + 0 0 24px -2px ${rgba(COLORS.cyan, 0.18)}, + inset 0 1px 0 rgba(255, 255, 255, 0.08); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + transition: border-color 0.4s ease, box-shadow 0.4s ease; +} +.dm-opt-floating-badge__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: ${COLORS.cyan}; + box-shadow: 0 0 10px ${COLORS.cyan}; + animation: dmOptPulse 1.4s ease-in-out infinite; + transition: background 0.4s ease, box-shadow 0.4s ease; +} +.dm-opt-floating-badge__dot.is-scan { + background: ${COLORS.cyan}; + box-shadow: 0 0 10px ${COLORS.cyan}; +} +.dm-opt-floating-badge__dot.is-dissolve, +.dm-opt-floating-badge__dot.is-optimize { + background: ${COLORS.amber}; + box-shadow: 0 0 10px ${COLORS.amber}; +} +.dm-opt-floating-badge__dot.is-reorganize { + background: #C084FC; + box-shadow: 0 0 10px #C084FC; +} +.dm-opt-floating-badge__dot.is-metrics { + background: ${COLORS.green}; + box-shadow: 0 0 10px ${COLORS.green}; +} +.dm-opt-floating-badge__text { + font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: #F8FAFC; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); +} + +/* ===== UI OVERLAY ===== */ +.dm-opt-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none; } +.dm-opt-ui h2, .dm-opt-ui h3, .dm-opt-metric__value, .dm-opt-eyebrow, .dm-opt-phase { + font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; +} + +/* ===== HEADER — compact, no dead space ===== */ +.dm-opt-head { + position: absolute; top: clamp(18px, 3vh, 36px); left: 50%; + transform: translateX(-50%); width: min(640px, 90vw); text-align: center; +} +.dm-opt-eyebrow { + display: inline-flex; align-items: center; gap: 7px; + font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; + color: ${COLORS.cyan}; padding: 5px 14px; border-radius: 999px; + background: ${rgba(COLORS.cyan, 0.06)}; border: 1px solid ${rgba(COLORS.cyan, 0.25)}; + backdrop-filter: blur(8px); +} +.dm-opt-dot { width: 6px; height: 6px; border-radius: 50%; background: ${COLORS.cyan}; box-shadow: 0 0 10px ${COLORS.cyan}; } +.dm-opt .dm-opt-head h2 { + margin: 8px 0 4px !important; padding: 0 !important; color: #F8FAFC !important; + font-weight: 700 !important; text-transform: none !important; + font-size: clamp(22px, 2.4vw, 36px) !important; line-height: 1.1 !important; + letter-spacing: -0.015em !important; +} +.dm-opt .dm-opt-head p { + margin: 0 auto !important; padding: 0 !important; color: ${COLORS.textDim} !important; + max-width: 440px; font-size: clamp(11px, 1vw, 13px) !important; line-height: 1.45 !important; +} + +/* ===== WORKFLOW STEPS ===== */ +.dm-opt-steps { + display: flex; align-items: center; justify-content: center; gap: 0; + margin-top: 12px; flex-wrap: wrap; +} +.dm-opt-steps__pill { + display: inline-flex; align-items: center; gap: 4px; + padding: 4px 10px; border-radius: 999px; font-weight: 600; + letter-spacing: 0.04em; text-transform: uppercase; + color: ${COLORS.slate}; + background: ${rgba(COLORS.ink, 0.5)}; + border: 1px solid ${rgba(COLORS.slate, 0.2)}; + backdrop-filter: blur(6px); + transition: all 0.45s cubic-bezier(0.22, 1, 0.36, 1); +} +.dm-opt-steps__pill.is-active { + color: #E2E8F0; + background: ${rgba(COLORS.cyan, 0.10)}; + border-color: ${rgba(COLORS.cyan, 0.35)}; + box-shadow: 0 0 18px -6px ${rgba(COLORS.cyan, 0.5)}; +} +.dm-opt-steps__pill.is-current { + color: ${COLORS.cyan}; + border-color: ${rgba(COLORS.cyan, 0.6)}; + box-shadow: 0 0 24px -4px ${rgba(COLORS.cyan, 0.6)}; +} +.dm-opt-steps__icon { font-size: 11px; } +.dm-opt-steps__text { font-size: 9.5px; } +.dm-opt-steps__line { + display: block; width: 16px; height: 1px; margin: 0 2px; + background: ${rgba(COLORS.slate, 0.3)}; + transition: background 0.45s ease; +} +.dm-opt-steps__line.is-active { + background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green}); + box-shadow: 0 0 6px ${rgba(COLORS.cyan, 0.5)}; +} + +/* ===== PROGRESS BAR ===== */ +.dm-opt-progress { margin-top: 10px; } +.dm-opt-progress__track { + height: 2px; border-radius: 999px; overflow: hidden; + background: ${rgba(COLORS.cyan, 0.10)}; max-width: 420px; margin: 0 auto; +} +.dm-opt-progress__fill { + height: 100%; border-radius: 999px; + background: linear-gradient(90deg, ${COLORS.cyan}, ${COLORS.green}); + box-shadow: 0 0 12px ${rgba(COLORS.cyan, 0.6)}; +} +.dm-opt-status { + display: inline-flex; align-items: center; gap: 8px; margin-top: 8px; + padding: 4px 11px; border-radius: 999px; + background: ${rgba(COLORS.ink, 0.55)}; border: 1px solid ${rgba(COLORS.cyan, 0.18)}; + backdrop-filter: blur(8px); +} +.dm-opt-status__dot { + width: 5px; height: 5px; border-radius: 50%; background: ${COLORS.cyan}; + box-shadow: 0 0 8px ${COLORS.cyan}; animation: dmOptPulse 1.4s ease-in-out infinite; +} +.dm-opt-status__label { + font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #E2E8F0; font-weight: 600; +} +.dm-opt-status__step { font-size: 9.5px; color: ${COLORS.cyan}; font-weight: 600; } + +/* ===== COMPARE PANELS — tighter, stronger ===== */ +.dm-opt-compare { + position: absolute; top: 50%; left: 0; right: 0; transform: translateY(-50%); + display: flex; justify-content: space-between; align-items: center; + gap: 12px; padding: 0 clamp(12px, 2.5vw, 36px); +} +.dm-opt-panel { + pointer-events: auto; width: clamp(230px, 26vw, 340px); + padding: 18px 20px; border-radius: 20px; + background: ${rgba(COLORS.ink, 0.72)}; + border: 1px solid ${rgba(COLORS.slate, 0.22)}; + backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); +} +.dm-opt-panel--bad { + border-color: ${rgba(COLORS.red, 0.45)}; + box-shadow: + 0 0 30px -8px ${rgba(COLORS.red, 0.35)}, + 0 20px 50px -20px ${rgba(COLORS.red, 0.25)}, + inset 0 1px 0 ${rgba(COLORS.red, 0.08)}; +} +.dm-opt-panel--good { + border-color: ${rgba(COLORS.green, 0.45)}; + box-shadow: + 0 0 30px -8px ${rgba(COLORS.green, 0.35)}, + 0 20px 50px -20px ${rgba(COLORS.green, 0.25)}, + inset 0 1px 0 ${rgba(COLORS.green, 0.08)}; +} +.dm-opt .dm-opt-panel h3 { + margin: 10px 0 12px !important; padding: 0 !important; color: #F1F5F9 !important; + font-size: clamp(15px, 1.4vw, 19px) !important; font-weight: 600 !important; + line-height: 1.15 !important; text-transform: none !important; letter-spacing: -0.01em !important; +} +.dm-opt .dm-opt-panel ul { list-style: none !important; margin: 0 !important; padding: 0 !important; display: grid; gap: 7px; } +.dm-opt .dm-opt-panel li { + position: relative; padding-left: 0 !important; margin: 0 !important; + color: ${COLORS.textDim} !important; font-size: 12.5px !important; line-height: 1.35 !important; + display: flex; align-items: center; gap: 8px; +} +.dm-opt .dm-opt-panel li::marker { content: "" !important; } +/* Remove old dot pseudo — now using ✖/✔ markers */ +.dm-opt .dm-opt-panel li::before { content: none !important; display: none !important; } + +/* ✖/✔ markers */ +.dm-opt-marker { + display: inline-flex; align-items: center; justify-content: center; + width: 18px; height: 18px; border-radius: 6px; flex-shrink: 0; + font-size: 10px; font-weight: 700; line-height: 1; +} +.dm-opt-marker--x { + background: ${rgba(COLORS.red, 0.15)}; + color: ${COLORS.red}; + border: 1px solid ${rgba(COLORS.red, 0.35)}; + box-shadow: 0 0 8px ${rgba(COLORS.red, 0.3)}; +} +.dm-opt-marker--ok { + background: ${rgba(COLORS.green, 0.15)}; + color: ${COLORS.green}; + border: 1px solid ${rgba(COLORS.green, 0.35)}; + box-shadow: 0 0 8px ${rgba(COLORS.green, 0.3)}; +} + +.dm-opt-panel__badge { + display: inline-flex; align-items: center; gap: 7px; + font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700; + color: ${COLORS.red}; padding: 5px 10px; border-radius: 999px; + background: ${rgba(COLORS.red, 0.1)}; border: 1px solid ${rgba(COLORS.red, 0.35)}; +} +.dm-opt-panel__badge--good { color: ${COLORS.green}; background: ${rgba(COLORS.green, 0.1)}; border-color: ${rgba(COLORS.green, 0.35)}; } +.dm-opt-pulse { width: 6px; height: 6px; border-radius: 50%; animation: dmOptPulse 1.4s ease-in-out infinite; } +.dm-opt-pulse--red { background: ${COLORS.red}; box-shadow: 0 0 10px ${COLORS.red}; } +.dm-opt-pulse--green { background: ${COLORS.green}; box-shadow: 0 0 10px ${COLORS.green}; } +@keyframes dmOptPulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.5; } } + +/* ===== METRICS ===== */ +.dm-opt-foot { + position: absolute; left: 0; right: 0; bottom: clamp(12px, 2.5vh, 28px); + padding: 0 clamp(12px, 3vw, 36px); +} +.dm-opt-metrics { + pointer-events: auto; display: grid; grid-template-columns: repeat(5, 1fr); + gap: 10px; max-width: 1100px; margin: 0 auto; +} +.dm-opt-metric { + position: relative; padding: 14px 14px 12px; border-radius: 16px; + background: ${rgba(COLORS.ink, 0.72)}; + border: 1px solid ${rgba(COLORS.slate, 0.25)}; + backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); + overflow: hidden; + opacity: 0; transform: translateY(22px); + animation: dmOptCardIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; + box-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.5), inset 0 1px 0 ${rgba("#ffffff", 0.04)}; + transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.4s ease, box-shadow 0.4s ease, background 0.4s ease !important; + cursor: pointer; +} +.dm-opt-metric:hover { + transform: translateY(-5px) scale(1.028) !important; + background: ${rgba(COLORS.ink, 0.9)} !important; + border-color: ${rgba(COLORS.cyan, 0.52)} !important; + box-shadow: + 0 18px 48px -8px ${rgba(COLORS.cyan, 0.25)}, + 0 4px 12px -2px ${rgba(COLORS.cyan, 0.12)}, + inset 0 1px 0 rgba(255, 255, 255, 0.1) !important; +} +.dm-opt-metric:hover .dm-opt-metric__sparkline { + opacity: 0.72 !important; +} +.dm-opt-metric__sparkline { + transition: opacity 0.4s ease; +} +@keyframes dmOptCardIn { to { opacity: 1; transform: translateY(0); } } +.dm-opt-metric__top { display: flex; align-items: center; justify-content: space-between; } +.dm-opt-metric__label { + font-size: 10px; letter-spacing: 0.04em; color: ${COLORS.textDim}; text-transform: uppercase; +} +.dm-opt-metric__arrow { font-size: 11px; } +.dm-opt-metric__value { margin-top: 6px; font-size: clamp(20px, 2.8vw, 32px); font-weight: 700; line-height: 1; } +.dm-opt-metric__bar { margin-top: 10px; height: 3px; border-radius: 999px; background: ${rgba(COLORS.slate, 0.2)}; overflow: hidden; } +.dm-opt-metric__fill { height: 100%; border-radius: 999px; transform-origin: left center; } + +/* ===== BOTTOM INSIGHT BAR ===== */ +.dm-opt-insight { + display: flex; align-items: center; justify-content: center; gap: 8px; + max-width: 760px; margin: 10px auto 0; + padding: 7px 18px; border-radius: 999px; + background: ${rgba(COLORS.ink, 0.6)}; + border: 1px solid ${rgba(COLORS.cyan, 0.12)}; + backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); + pointer-events: auto; +} +.dm-opt-insight__dot { + width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; + background: ${COLORS.green}; + box-shadow: 0 0 12px ${COLORS.green}; + animation: dmOptPulse 2s ease-in-out infinite; +} +.dm-opt-insight__text { + font-size: 10.5px; color: ${COLORS.textDim}; line-height: 1.3; + letter-spacing: 0.03em; font-weight: 500; +} +.dm-opt-insight__text strong { + color: #E2E8F0; font-weight: 700; +} +.dm-opt-insight__sep { + width: 1px; height: 12px; flex-shrink: 0; + background: ${rgba(COLORS.slate, 0.35)}; +} + +/* ===== RESPONSIVE ===== */ +@media (max-width: 1024px) { + .dm-opt-panel { width: clamp(190px, 30vw, 280px); padding: 14px 16px; } + .dm-opt-panel li { font-size: 11.5px; } + .dm-opt-marker { width: 16px; height: 16px; font-size: 9px; } +} +@media (max-width: 767px) { + .dm-opt { height: 200vh; } + .dm-opt-card { + top: 8px !important; left: 8px !important; right: 8px !important; bottom: 8px !important; + border-radius: 24px !important; + } + .dm-opt-compare { + top: auto; bottom: 148px; transform: none; + flex-direction: row; align-items: stretch; gap: 6px; padding: 0 10px; + } + .dm-opt-panel { width: 50%; padding: 10px 11px; border-radius: 14px; } + .dm-opt-panel ul { gap: 4px; } + .dm-opt-panel li { font-size: 10px; } + .dm-opt-panel li::before { display: none !important; } + .dm-opt-marker { width: 14px; height: 14px; font-size: 8px; border-radius: 4px; } + .dm-opt-panel h3 { margin: 6px 0 5px; font-size: 14px; } + .dm-opt-panel__badge { font-size: 8px; padding: 3px 7px; } + .dm-opt-metrics { grid-template-columns: repeat(5, 1fr); gap: 4px; } + .dm-opt-metric { padding: 7px 5px; border-radius: 10px; } + .dm-opt-metric__label { font-size: 7.5px; letter-spacing: 0; } + .dm-opt-metric__value { font-size: 14px; } + .dm-opt-metric__bar { margin-top: 6px; } + .dm-opt-head h2 { font-size: 22px; margin: 6px 0 4px; } + .dm-opt-head p { font-size: 10.5px; } + .dm-opt-phases { display: none; } + .dm-opt-steps { gap: 0; } + .dm-opt-steps__pill { padding: 3px 6px; } + .dm-opt-steps__icon { font-size: 9px; } + .dm-opt-steps__text { font-size: 7.5px; } + .dm-opt-steps__line { width: 6px; } + .dm-opt-insight { padding: 5px 10px; gap: 5px; } + .dm-opt-insight__text { font-size: 8.5px; } + .dm-opt-insight__sep { height: 10px; } +} +@media (prefers-reduced-motion: reduce) { + .dm-opt-pulse { animation: none; } + .dm-opt-metric { animation: none; opacity: 1; transform: none; } + .dm-opt-insight__dot { animation: none; } + .dm-opt-card::before { animation: none; } +} +`; diff --git a/src/components/optimization/RouteSystem.tsx b/src/components/optimization/RouteSystem.tsx new file mode 100644 index 0000000..3552132 --- /dev/null +++ b/src/components/optimization/RouteSystem.tsx @@ -0,0 +1,514 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; +import { COLORS } from "./constants"; +import { buildRoutes } from "./routes"; +import { smoothstep, seeded } from "./math"; + +type Props = { + progress: React.RefObject; + reduced?: boolean; + isMobile?: boolean; +}; + +const RADIAL = 6; +const TUBULAR = 120; + +type TubeHandle = { geo: THREE.TubeGeometry; mat: THREE.MeshBasicMaterial }; + +type PulsingRingProps = { + position: THREE.Vector3; + color: string; + maxScale?: number; + pulseSpeed?: number; +}; + +/** + * Clean floating radar ring that pulses outwards and fades. + */ +function PulsingRing({ position, color, maxScale = 2.2, pulseSpeed = 2.4 }: PulsingRingProps) { + const ref = useRef(null); + const matRef = useRef(null); + + useFrame((state) => { + if (!ref.current || !matRef.current) return; + const t = state.clock.elapsedTime; + const cycle = (t * (pulseSpeed / 4)) % 1; // 0 -> 1 loop + const s = 0.4 + cycle * (maxScale - 0.4); + ref.current.scale.setScalar(s); + matRef.current.opacity = Math.sin((1 - cycle) * Math.PI) * 0.72; + }); + + return ( + + + + + ); +} + +const PulsingRingMemo = React.memo(PulsingRing); + +/* ── Chaotic Route Flow Particles & Chevrons ── */ +const CHAOS_TMP_POS = new THREE.Vector3(); +const CHAOS_TMP_TAN = new THREE.Vector3(); + +function ChaoticFlow({ progress, chaoticCurves }: { + progress: React.RefObject; + chaoticCurves: THREE.CatmullRomCurve3[]; +}) { + const particlesPerCurve = 4; + const count = chaoticCurves.length * particlesPerCurve; + const particleRefs = useRef<(THREE.Mesh | null)[]>([]); + const chevronRefs = useRef<(THREE.Group | null)[]>([]); + + const particleOffsets = useMemo( + () => Array.from({ length: count }, (_, i) => seeded(i * 13 + 3)), + [count] + ); + const chevronOffsets = useMemo( + () => Array.from({ length: chaoticCurves.length * 2 }, (_, i) => (i % 2) / 2), + [chaoticCurves.length] + ); + + useFrame((state) => { + const p = progress.current ?? 0; + const vis = 1 - smoothstep(0.48, 0.60, p); + + const isVisible = vis > 0.01; + particleRefs.current.forEach(m => { if (m) m.visible = isVisible; }); + chevronRefs.current.forEach(g => { if (g) g.visible = isVisible; }); + if (!isVisible) return; + + const t = state.clock.elapsedTime; + + // Animate tiny red flow particles + for (let i = 0; i < count; i++) { + const mesh = particleRefs.current[i]; + if (!mesh) continue; + const curveIdx = Math.floor(i / particlesPerCurve); + const curve = chaoticCurves[curveIdx]; + + const speed = 0.05 + seeded(i * 9) * 0.03; + const u = (particleOffsets[i] + t * speed) % 1; + + curve.getPointAt(u, CHAOS_TMP_POS); + mesh.position.copy(CHAOS_TMP_POS); + + const mat = mesh.material as THREE.MeshBasicMaterial; + mat.opacity = vis * 0.6 * Math.sin(u * Math.PI); + } + + // Animate red chevrons (erratic flow) + for (let i = 0; i < chaoticCurves.length * 2; i++) { + const group = chevronRefs.current[i]; + if (!group) continue; + const curveIdx = Math.floor(i / 2); + const curve = chaoticCurves[curveIdx]; + + const u = (chevronOffsets[i] + t * 0.04) % 1; + curve.getPointAt(u, CHAOS_TMP_POS); + curve.getTangentAt(u, CHAOS_TMP_TAN); + + group.position.copy(CHAOS_TMP_POS); + group.rotation.y = Math.atan2(CHAOS_TMP_TAN.x, CHAOS_TMP_TAN.z); + + group.traverse((o) => { + const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined; + if (m && "opacity" in m) { + m.opacity = vis * 0.5 * Math.sin(u * Math.PI); + } + }); + } + }); + + return ( + + {/* Chaotic Red Flow Particles */} + {Array.from({ length: count }).map((_, i) => ( + { particleRefs.current[i] = el; }} + visible={false} + > + + + + ))} + + {/* Chaotic Red Chevrons */} + {Array.from({ length: chaoticCurves.length * 2 }).map((_, i) => ( + { chevronRefs.current[i] = el; }} + visible={false} + > + + + + + + ))} + + ); +} + +const ChaoticFlowMemo = React.memo(ChaoticFlow); + +/* ── Optimized Route Flow Particles, Chevrons & Delivery Pulses ── */ +const OPT_TMP_POS = new THREE.Vector3(); +const OPT_TMP_TAN = new THREE.Vector3(); + +function OptimizedFlow({ progress, optimizedCurves }: { + progress: React.RefObject; + optimizedCurves: THREE.CatmullRomCurve3[]; +}) { + const particlesPerCurve = 6; + const count = optimizedCurves.length * particlesPerCurve; + const particleRefs = useRef<(THREE.Mesh | null)[]>([]); + const chevronRefs = useRef<(THREE.Group | null)[]>([]); + const pulseRefs = useRef<(THREE.Mesh | null)[]>([]); + + const particleOffsets = useMemo( + () => Array.from({ length: count }, (_, i) => seeded(i * 17 + 4)), + [count] + ); + const chevronOffsets = useMemo( + () => Array.from({ length: optimizedCurves.length * 3 }, (_, i) => (i % 3) / 3), + [optimizedCurves.length] + ); + + useFrame((state) => { + const p = progress.current ?? 0; + const vis = smoothstep(0.52, 0.70, p); + + const isVisible = vis > 0.01; + particleRefs.current.forEach(m => { if (m) m.visible = isVisible; }); + chevronRefs.current.forEach(g => { if (g) g.visible = isVisible; }); + pulseRefs.current.forEach(m => { if (m) m.visible = isVisible; }); + if (!isVisible) return; + + const t = state.clock.elapsedTime; + + // 1. Animate smooth green/cyan flow particles + for (let i = 0; i < count; i++) { + const mesh = particleRefs.current[i]; + if (!mesh) continue; + const curveIdx = Math.floor(i / particlesPerCurve); + const curve = optimizedCurves[curveIdx]; + + const speed = 0.08 + seeded(i * 11) * 0.04; + const u = (particleOffsets[i] + t * speed) % 1; + + curve.getPointAt(u, OPT_TMP_POS); + mesh.position.copy(OPT_TMP_POS); + + const mat = mesh.material as THREE.MeshBasicMaterial; + mat.opacity = vis * 0.75 * Math.sin(u * Math.PI); + const s = 0.04 + Math.sin(t * 6 + i) * 0.012; + mesh.scale.setScalar(s / 0.05); + } + + // 2. Animate direction chevrons (fast, steady green chevrons) + for (let i = 0; i < optimizedCurves.length * 3; i++) { + const group = chevronRefs.current[i]; + if (!group) continue; + const curveIdx = Math.floor(i / 3); + const curve = optimizedCurves[curveIdx]; + + const u = (chevronOffsets[i] + t * 0.16) % 1; + curve.getPointAt(u, OPT_TMP_POS); + curve.getTangentAt(u, OPT_TMP_TAN); + + group.position.copy(OPT_TMP_POS); + group.rotation.y = Math.atan2(OPT_TMP_TAN.x, OPT_TMP_TAN.z); + group.rotation.x = Math.atan2(OPT_TMP_TAN.y, Math.sqrt(OPT_TMP_TAN.x * OPT_TMP_TAN.x + OPT_TMP_TAN.z * OPT_TMP_TAN.z)) - Math.PI / 2; + + group.traverse((o) => { + const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined; + if (m && "opacity" in m) { + m.opacity = vis * 0.85 * Math.sin(u * Math.PI); + } + }); + } + + // 3. Animate expanding delivery pulses (shooting data pulses) + for (let i = 0; i < optimizedCurves.length; i++) { + const mesh = pulseRefs.current[i]; + if (!mesh) continue; + const curve = optimizedCurves[i]; + + const u = (t * 0.55 + i * 0.2) % 1; + curve.getPointAt(u, OPT_TMP_POS); + mesh.position.copy(OPT_TMP_POS); + + const mat = mesh.material as THREE.MeshBasicMaterial; + mat.opacity = vis * 0.9 * Math.sin(u * Math.PI); + const s = 0.08 + Math.sin(t * 12 + i) * 0.02; + mesh.scale.setScalar(s / 0.07); + } + }); + + return ( + + {/* Tiny Cyan/Green Flow Particles */} + {Array.from({ length: count }).map((_, i) => { + const color = (Math.floor(i / particlesPerCurve) % 2 === 0) ? COLORS.cyan : COLORS.green; + return ( + { particleRefs.current[i] = el; }} + visible={false} + > + + + + ); + })} + + {/* Glowing Chevrons */} + {Array.from({ length: optimizedCurves.length * 3 }).map((_, i) => { + const color = (Math.floor(i / 3) % 2 === 0) ? COLORS.cyan : COLORS.green; + return ( + { chevronRefs.current[i] = el; }} + visible={false} + > + + + + + + ); + })} + + {/* Large Delivery Pulses */} + {optimizedCurves.map((_, i) => { + const color = (i % 2 === 0) ? COLORS.cyan : COLORS.green; + return ( + { pulseRefs.current[i] = el; }} + visible={false} + > + + + + ); + })} + + ); +} + +const OptimizedFlowMemo = React.memo(OptimizedFlow); + +/** + * Glowing route network. Chaotic red routes are pre-drawn then "erased" via + * geometry draw-range during the dissolve phase; optimized cyan/green routes + * draw themselves in during the optimize phase. Delivery nodes flip red→green. + */ +function RouteSystem({ progress, reduced = false, isMobile = false }: Props) { + const { chaotic, optimized, chaosNodes, optimizedNodes } = useMemo( + () => buildRoutes(), + [], + ); + + const chaosRefs = useRef([]); + const optRefs = useRef([]); + const chaosNodeGroup = useRef(null); + const optNodeGroup = useRef(null); + + const registerChaos = (i: number) => (geo: THREE.TubeGeometry | null) => { + if (geo) chaosRefs.current[i] = { ...(chaosRefs.current[i] ?? {}), geo } as TubeHandle; + }; + const registerChaosMat = (i: number) => (mat: THREE.MeshBasicMaterial | null) => { + if (mat) chaosRefs.current[i] = { ...(chaosRefs.current[i] ?? {}), mat } as TubeHandle; + }; + const registerOpt = (i: number) => (geo: THREE.TubeGeometry | null) => { + if (geo) optRefs.current[i] = { ...(optRefs.current[i] ?? {}), geo } as TubeHandle; + }; + const registerOptMat = (i: number) => (mat: THREE.MeshBasicMaterial | null) => { + if (mat) optRefs.current[i] = { ...(optRefs.current[i] ?? {}), mat } as TubeHandle; + }; + + const setDraw = (geo: THREE.TubeGeometry, frac: number) => { + if (!geo.index) return; + const total = geo.index.count; + const step = RADIAL * 6; + const count = Math.max(0, Math.floor((total * frac) / step) * step); + geo.setDrawRange(0, count); + }; + + useFrame((state) => { + const p = progress.current ?? 0; + const t = state.clock.elapsedTime; + + // Chaotic routes: full → erased during dissolve, with warning flicker. + const chaosDraw = 1 - smoothstep(0.4, 0.56, p); + const chaosBase = (1 - smoothstep(0.38, 0.55, p)) * 0.85; + const flicker = 0.7 + Math.sin(t * 7) * 0.18; + for (const h of chaosRefs.current) { + if (!h?.geo) continue; + setDraw(h.geo, chaosDraw); + if (h.mat) h.mat.opacity = chaosBase * flicker; + } + + // Optimized routes: draw-in during optimize phase + flowing glow. + const optDraw = smoothstep(0.55, 0.74, p); + const optBase = smoothstep(0.55, 0.66, p); + for (let i = 0; i < optRefs.current.length; i++) { + const h = optRefs.current[i]; + if (!h?.geo) continue; + setDraw(h.geo, optDraw); + if (h.mat) { + // Keep the tube a crisp, saturated energy line rather than a + // bloom-blown white slab — the flow energy comes from the pulses + trails. + const flow = 0.52 + Math.sin(t * 3 - i * 0.7) * 0.12; + h.mat.opacity = optBase * flow; + } + } + + // Delivery nodes + if (chaosNodeGroup.current) { + const s = 0.0001 + (1 - smoothstep(0.4, 0.55, p)); + chaosNodeGroup.current.scale.setScalar(s); + } + if (optNodeGroup.current) { + const appear = smoothstep(0.6, 0.82, p); + const pop = appear * (1 + Math.sin(t * 4) * 0.06 * appear); + optNodeGroup.current.scale.setScalar(0.0001 + pop); + } + }); + + return ( + + {/* Chaotic routes (red) */} + {chaotic.map((r, i) => ( + + + + + ))} + + {/* Optimized routes (cyan → green) */} + {optimized.map((r, i) => ( + + + + + ))} + + {/* Chaos delivery nodes (red, delayed) with pulsing warning alerts */} + + {chaosNodes.map((n, i) => ( + + + + + + + + ))} + + + {/* Optimized delivery nodes (green, fulfilled) with pulsing success alerts */} + + {optimizedNodes.map((n, i) => ( + + + + + + + + ))} + + + {/* Flowing delivery particles along routes */} + {!isMobile && ( + <> + r.curve)} + /> + r.curve)} + /> + + )} + + ); +} + +export default React.memo(RouteSystem); diff --git a/src/components/optimization/VehicleFleet.tsx b/src/components/optimization/VehicleFleet.tsx new file mode 100644 index 0000000..20a3333 --- /dev/null +++ b/src/components/optimization/VehicleFleet.tsx @@ -0,0 +1,294 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; +import { COLORS } from "./constants"; +import { buildRoutes, RouteDef } from "./routes"; +import { seeded, smoothstep } from "./math"; + +type Props = { + progress: React.RefObject; + reduced?: boolean; +}; + +type VType = "truck" | "van" | "bike"; + +type VehicleDef = { + curve: THREE.CatmullRomCurve3; + type: VType; + speed: number; + offset: number; + color: string; + nodes: THREE.Vector3[]; +}; + +const TMP_POS = new THREE.Vector3(); +const TMP_TAN = new THREE.Vector3(); +const TRAIL_TMP = new THREE.Vector3(); + +// Glowing energy comet-tail trailing each optimized vehicle (see ref image: +// a delivery van with flowing green light streaks). Sampled along the vehicle's +// own route curve behind its current position, so it always hugs the track and +// never smears on a scroll-jump. +const TRAIL_N = 16; +const TRAIL_GAP = 0.011; + +/** Flat billboarded truck cutout from the user's artwork (modelled facing +Z; + * the fleet loop rotates each one to face the camera). */ +function TruckBillboard({ + texture, + tint, + width, + aspect, +}: { + texture: THREE.Texture; + tint: string; + width: number; + aspect: number; +}) { + const h = width / aspect; + return ( + + + + + ); +} + +const TruckBillboardMemo = React.memo(TruckBillboard); + +const HUB = new THREE.Vector3(0, 0.5, 0); + +/** + * Moving fleet. 12 erratic vehicles ride the chaotic routes; on the reorganize + * phase they fade out and 8 optimally-assigned vehicles take over the clean + * routes. Position + heading are driven along the shared curves each frame. + */ +function VehicleFleet({ progress, reduced = false }: Props) { + const { chaotic, optimized } = useMemo(() => buildRoutes(), []); + + const types: VType[] = ["truck", "van", "bike"]; + + const chaosFleet = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => ({ + curve: (chaotic[i % chaotic.length] as RouteDef).curve, + type: types[i % 3], + speed: 0.018 + seeded(i * 5 + 1) * 0.03, + offset: seeded(i * 5 + 2), + color: COLORS.red, + nodes: (chaotic[i % chaotic.length] as RouteDef).nodes || [], + })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chaotic]); + + const optFleet = useMemo(() => { + return Array.from({ length: 5 }, (_, i) => ({ + curve: (optimized[i % optimized.length] as RouteDef).curve, + type: types[i % 3], + speed: 0.05 + seeded(i * 7 + 1) * 0.025, + offset: seeded(i * 7 + 2), + color: i % 2 === 0 ? COLORS.cyan : COLORS.green, + nodes: (optimized[i % optimized.length] as RouteDef).nodes || [], + })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [optimized]); + + const chaosRefs = useRef<(THREE.Group | null)[]>([]); + const optRefs = useRef<(THREE.Group | null)[]>([]); + const optTrailRefs = useRef<(THREE.Mesh | null)[]>([]); + + // The optimized fleet uses the user's isometric truck artwork as a flat + // billboarded cutout (loaded imperatively so we don't need a Suspense fence). + const truckTex = useMemo(() => { + const t = new THREE.TextureLoader().load("/images/truck.png"); + t.colorSpace = THREE.SRGBColorSpace; + t.anisotropy = 8; + return t; + }, []); + const TRUCK_ASPECT = 1080 / 948; // rasterized PNG dimensions + const TRUCK_W = 1.75; + + // Persistent progress values + const chaosProgress = useRef([]); + const optProgress = useRef([]); + + // Re-sync on length change (not just when empty) so a fleet-size edit / HMR + // doesn't leave indices reading `undefined` and crashing getPointAt(). + if (chaosProgress.current.length !== chaosFleet.length) { + chaosProgress.current = chaosFleet.map((v) => v.offset); + } + if (optProgress.current.length !== optFleet.length) { + optProgress.current = optFleet.map((v) => v.offset); + } + + const place = ( + group: THREE.Group | null, + def: VehicleDef, + progressArray: number[], + index: number, + dt: number, + opacity: number, + ) => { + if (!group || !def?.curve) return; + const visible = opacity > 0.02; + group.visible = visible; + if (!visible) return; + + let u = progressArray[index]; + if (u === undefined || Number.isNaN(u)) u = progressArray[index] = def.offset ?? 0; + + // Get position to compute distance to nodes + def.curve.getPointAt(u, TMP_POS); + + // Compute distance to nearest node + let minDist = TMP_POS.distanceTo(HUB); + if (def.nodes && def.nodes.length > 0) { + def.nodes.forEach((node) => { + const dist = TMP_POS.distanceTo(node); + if (dist < minDist) minDist = dist; + }); + } + + // Slow at nodes (speed reduction to 20%), accelerate on long corridors (up to 125%) + const speedFactor = 0.20 + smoothstep(0.4, 2.5, minDist) * 1.05; + + // Increment progress continuously + u = (u + dt * def.speed * speedFactor) % 1; + progressArray[index] = u; + + // Get final position and orientation + def.curve.getPointAt(u, TMP_POS); + def.curve.getTangentAt(u, TMP_TAN); + + group.position.copy(TMP_POS); + group.rotation.y = Math.atan2(TMP_TAN.x, TMP_TAN.z); + + group.traverse((o) => { + const m = (o as THREE.Mesh).material as THREE.Material | undefined; + if (m && "opacity" in m) { + (m as THREE.MeshBasicMaterial).opacity = opacity * baseOpacity(o); + } + }); + }; + + // preserve per-material relative opacity (fill vs wire vs light) + const baseOpacity = (o: THREE.Object3D) => { + const m = (o as THREE.Mesh).material as THREE.MeshBasicMaterial | undefined; + if (!m) return 1; + if (m.map) return 1; // textured truck cutout fades at full strength + if ((m as THREE.MeshBasicMaterial).wireframe) return 1; + if (m.color && m.color.getHex() === 0xffffff) return 1; + if (m.color && m.color.getHex() === 0xef4444) return 1; // headlight/taillight colors + return 0.2; + }; + + useFrame((state, dt) => { + const p = progress.current ?? 0; + const t = state.clock.elapsedTime; + const safeDt = Math.min(0.06, dt); + + const chaosFadeIn = smoothstep(0.10, 0.22, p); + const chaosFadeOut = 1 - smoothstep(0.70, 0.82, p); + const chaosOp = chaosFadeIn * chaosFadeOut * (0.85 + Math.sin(t * 6) * 0.1); + const optOp = smoothstep(0.68, 0.82, p); + + const cam = state.camera; + for (let i = 0; i < chaosFleet.length; i++) { + place(chaosRefs.current[i], chaosFleet[i], chaosProgress.current, i, safeDt, chaosOp); + const g = chaosRefs.current[i]; + if (g && g.visible) { + g.rotation.y = Math.atan2(cam.position.x - g.position.x, cam.position.z - g.position.z); + } + } + for (let i = 0; i < optFleet.length; i++) { + place(optRefs.current[i], optFleet[i], optProgress.current, i, safeDt, optOp); + // The truck is 2D cutout art, so billboard it around Y to always face the + // camera (overrides the tangent heading set inside place()). + const g = optRefs.current[i]; + if (g && g.visible) { + g.rotation.y = Math.atan2(cam.position.x - g.position.x, cam.position.z - g.position.z); + } + // Energy comet-tail behind each optimized vehicle. + const u = optProgress.current[i]; + const curve = optFleet[i]?.curve; + if (!curve || u === undefined || Number.isNaN(u)) continue; + for (let k = 0; k < TRAIL_N; k++) { + const mesh = optTrailRefs.current[i * TRAIL_N + k]; + if (!mesh) continue; + if (optOp < 0.02) { mesh.visible = false; continue; } + let uk = u - (k + 1) * TRAIL_GAP; + if (uk < 0) uk = 0; + mesh.visible = true; + curve.getPointAt(uk, TRAIL_TMP); + mesh.position.copy(TRAIL_TMP); + const taper = 1 - k / TRAIL_N; + const size = 0.05 + taper * 0.13; + mesh.scale.setScalar(size / 0.1); + const mat = mesh.material as THREE.MeshBasicMaterial; + mat.opacity = optOp * taper * taper * (0.65 + Math.sin(t * 9 - k * 0.7) * 0.35); + } + } + }); + + return ( + + {/* Chaotic fleet — the user's truck art tinted red (congested/before) */} + {chaosFleet.map((_v, i) => ( + { + chaosRefs.current[i] = el; + }} + > + + + ))} + {/* Optimized fleet — the truck art in clean white */} + {optFleet.map((_v, i) => ( + { + optRefs.current[i] = el; + }} + > + + + ))} + + {/* Glowing energy comet-tails for the optimized fleet */} + {optFleet.map((v, i) => + Array.from({ length: TRAIL_N }, (_, k) => ( + { + optTrailRefs.current[i * TRAIL_N + k] = el; + }} + > + + + + )), + )} + + ); +} + +export default React.memo(VehicleFleet); + + diff --git a/src/components/optimization/constants.ts b/src/components/optimization/constants.ts new file mode 100644 index 0000000..4d98615 --- /dev/null +++ b/src/components/optimization/constants.ts @@ -0,0 +1,118 @@ +/** + * Shared design tokens + KPI data for the AI Logistics Optimization section. + * Centralizing here keeps the 3D scene, overlay and metrics panel in sync. + */ + +export const COLORS = { + bg: "#020617", + cyan: "#00E5FF", + green: "#22C55E", + red: "#EF4444", + amber: "#F59E0B", + ink: "#0B1220", + slate: "#64748B", + textDim: "rgba(226,232,240,0.66)", +} as const; + +/** rgba helper used by inline styles + scoped CSS. */ +export function rgba(hex: string, alpha: number): string { + const h = hex.replace("#", ""); + const r = parseInt(h.substring(0, 2), 16); + const g = parseInt(h.substring(2, 4), 16); + const b = parseInt(h.substring(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +export type Kpi = { + key: string; + /** numeric value the counter animates between */ + before: number; + after: number; + prefixBefore?: string; + prefixAfter?: string; + suffix?: string; + labelBefore: string; + labelAfter: string; + /** lower number is the better outcome (drives the up/down arrow) */ + goodWhenLower: boolean; +}; + +export const KPIS: Kpi[] = [ + { + key: "distance", + before: 312, + after: 182, + suffix: " km", + labelBefore: "Route Distance", + labelAfter: "Route Distance", + goodWhenLower: true, + }, + { + key: "vehicles", + before: 8, + after: 5, + labelBefore: "Vehicles", + labelAfter: "Vehicles", + goodWhenLower: true, + }, + { + key: "orders", + before: 59, + after: 59, + labelBefore: "Orders", + labelAfter: "Orders Fulfilled", + goodWhenLower: false, + }, + { + key: "delayed", + before: 23, + after: 0, + labelBefore: "Delayed", + labelAfter: "Delayed", + goodWhenLower: true, + }, + { + key: "cost", + before: 18, + after: 18, + prefixBefore: "+", + prefixAfter: "−", + suffix: "%", + labelBefore: "Cost Overrun", + labelAfter: "Cost Saved", + goodWhenLower: false, + }, +]; + +/** + * Scroll-phase thresholds (normalized 0 → 1). The whole narrative is driven by + * a single scroll progress value shared between GSAP, R3F and Framer Motion. + */ +export const PHASES = { + chaos: 0.0, // Stage 1 & 2: Network appears & vehicles active + scan: 0.28, // Stage 3: AI scans routes + dissolve: 0.42, // Stage 4 & 5: Optimize begins & bad routes dissolve + optimize: 0.56, // Stage 6: Optimized routes appear + reorganize: 0.70, // Reassigning vehicles + metrics: 0.84, // Stage 7: KPIs update & complete +} as const; + +export type PhaseKey = keyof typeof PHASES; + +export function phaseFromProgress(p: number): PhaseKey { + if (p >= PHASES.metrics) return "metrics"; + if (p >= PHASES.reorganize) return "reorganize"; + if (p >= PHASES.optimize) return "optimize"; + if (p >= PHASES.dissolve) return "dissolve"; + if (p >= PHASES.scan) return "scan"; + return "chaos"; +} + +export const PHASE_LABELS: Record = { + chaos: "Monitoring network", + scan: "AI SCANNING NETWORK", + dissolve: "AI OPTIMIZING ROUTES", + optimize: "AI OPTIMIZING ROUTES", + reorganize: "AI REASSIGNING VEHICLES", + metrics: "AI COMPLETE", +}; diff --git a/src/components/optimization/math.ts b/src/components/optimization/math.ts new file mode 100644 index 0000000..fd0be33 --- /dev/null +++ b/src/components/optimization/math.ts @@ -0,0 +1,36 @@ +/** Small framerate-independent math helpers shared by the 3D scene. */ + +export const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v); + +export const lerp = (a: number, b: number, t: number) => a + (b - a) * t; + +/** Linear remap of `v` from [inMin,inMax] to [outMin,outMax], clamped. */ +export function remap( + v: number, + inMin: number, + inMax: number, + outMin = 0, + outMax = 1, +): number { + const t = clamp01((v - inMin) / (inMax - inMin || 1)); + return outMin + (outMax - outMin) * t; +} + +export function smoothstep(edge0: number, edge1: number, x: number): number { + const t = clamp01((x - edge0) / (edge1 - edge0 || 1)); + return t * t * (3 - 2 * t); +} + +/** + * Exponential damp toward a target — frame-rate independent. + * lambda ~ responsiveness (higher = snappier). + */ +export function damp(current: number, target: number, lambda: number, dt: number): number { + return lerp(current, target, 1 - Math.exp(-lambda * dt)); +} + +/** Deterministic pseudo-random in [0,1) — avoids Math.random for stable SSR/scene. */ +export function seeded(i: number): number { + const x = Math.sin(i * 127.1 + 311.7) * 43758.5453; + return x - Math.floor(x); +} diff --git a/src/components/optimization/routes.ts b/src/components/optimization/routes.ts new file mode 100644 index 0000000..570a661 --- /dev/null +++ b/src/components/optimization/routes.ts @@ -0,0 +1,91 @@ +import * as THREE from "three"; +import { seeded } from "./math"; + +/** + * Deterministic route + delivery-node generation shared by RouteSystem and + * VehicleFleet so glowing paths and moving vehicles stay perfectly aligned. + */ + +export const HUB = new THREE.Vector3(0, 0.5, 0); +export const ROUTE_Y = 0.45; + +export type RouteDef = { + curve: THREE.CatmullRomCurve3; + nodes: THREE.Vector3[]; +}; + +export type SceneRoutes = { + chaotic: RouteDef[]; + optimized: RouteDef[]; + chaosNodes: THREE.Vector3[]; + optimizedNodes: THREE.Vector3[]; +}; + +function arc(points: THREE.Vector3[]): THREE.CatmullRomCurve3 { + return new THREE.CatmullRomCurve3(points, false, "catmullrom", 0.5); +} + +let cached: SceneRoutes | null = null; + +export function buildRoutes(): SceneRoutes { + if (cached) return cached; + + // --- Optimized clusters: 5 tidy zones around the hub -------------------- + const clusterCount = 5; + const optimized: RouteDef[] = []; + const optimizedNodes: THREE.Vector3[] = []; + + for (let c = 0; c < clusterCount; c++) { + const baseAngle = (c / clusterCount) * Math.PI * 2 + 0.3; + const radius = 7 + seeded(c * 7 + 1) * 3; + const cx = Math.cos(baseAngle) * radius; + const cz = Math.sin(baseAngle) * radius; + + // 2-3 stops per cluster (multi-trip planning) + const stops = 2 + Math.floor(seeded(c * 7 + 2) * 2); + const pts: THREE.Vector3[] = [HUB.clone()]; + // gentle lift-off control point + pts.push(new THREE.Vector3(cx * 0.35, ROUTE_Y + 1.4, cz * 0.35)); + + const nodes: THREE.Vector3[] = []; + for (let s = 0; s < stops; s++) { + const jx = (seeded(c * 31 + s * 5 + 3) - 0.5) * 3.2; + const jz = (seeded(c * 31 + s * 5 + 4) - 0.5) * 3.2; + const node = new THREE.Vector3(cx + jx, ROUTE_Y, cz + jz); + nodes.push(node); + optimizedNodes.push(node); + pts.push(node.clone()); + } + optimized.push({ curve: arc(pts), nodes }); + } + + // --- Chaotic routes: tangled, overlapping, wandering -------------------- + const chaotic: RouteDef[] = []; + const chaosNodes: THREE.Vector3[] = []; + const chaosCount = 6; + + for (let r = 0; r < chaosCount; r++) { + const segs = 4 + Math.floor(seeded(r * 13 + 1) * 3); + const pts: THREE.Vector3[] = [HUB.clone()]; + let angle = seeded(r * 13 + 2) * Math.PI * 2; + const nodes: THREE.Vector3[] = []; + for (let s = 0; s < segs; s++) { + angle += (seeded(r * 13 + s * 3 + 3) - 0.5) * 2.6; // erratic turns + const reach = 2.6 + seeded(r * 13 + s * 3 + 4) * 6.4; // tighter, more central + const px = Math.cos(angle) * reach + (seeded(r * 17 + s) - 0.5) * 2.6; + const pz = Math.sin(angle) * reach + (seeded(r * 19 + s) - 0.5) * 2.6; + const py = ROUTE_Y + seeded(r * 23 + s) * 1.1; + const p = new THREE.Vector3(px, py, pz); + pts.push(p); + if (s === segs - 1) { + const node = new THREE.Vector3(px, ROUTE_Y, pz); + chaosNodes.push(node); + nodes.push(node); + } + } + chaotic.push({ curve: arc(pts), nodes }); + } + + cached = { chaotic, optimized, chaosNodes, optimizedNodes }; + return cached; +} diff --git a/src/components/performance/PerformanceCanvas.tsx b/src/components/performance/PerformanceCanvas.tsx new file mode 100644 index 0000000..bc496bb --- /dev/null +++ b/src/components/performance/PerformanceCanvas.tsx @@ -0,0 +1,464 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import { Canvas, useFrame } from "@react-three/fiber"; +import { Html } from "@react-three/drei"; +import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing"; +import { KernelSize } from "postprocessing"; +import * as THREE from "three"; +import { buildPerfRoutes, GATEWAY, LEFT_C, RIGHT_C, ROUTE_Y } from "./perfRoutes"; +import { SplineRider, VType } from "./Vehicles"; + +type Props = { + progress: React.RefObject; + reduced?: boolean; + isMobile?: boolean; + active?: boolean; +}; + +// Local math helpers (self-contained; avoids fragile cross-folder const imports). +const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v); +const lerp = (a: number, b: number, t: number) => a + (b - a) * t; +const smoothstep = (e0: number, e1: number, x: number) => { + const t = clamp01((x - e0) / (e1 - e0 || 1)); + return t * t * (3 - 2 * t); +}; +const damp = (cur: number, target: number, lambda: number, dt: number) => + lerp(cur, target, 1 - Math.exp(-lambda * dt)); +const seeded = (i: number) => { + const x = Math.sin(i * 127.1 + 311.7) * 43758.5453; + return x - Math.floor(x); +}; + +// Grounded "operational" palette — white / gray / green / orange / red. No cyan/blue/purple. +const C = { + bg: "#14171c", + ground: "#262b33", + road: "#2b3038", + bldA: "#4b5563", + bldB: "#6b7280", + bldC: "#9ca3af", + white: "#e5e7eb", + red: "#ef4444", + orange: "#f97316", + amber: "#fbbf24", + green: "#22c55e", + green2: "#4ade80", + steel: "#475569", +}; + +/* ───────────── Camera: grounded city flythrough (not orbit) ───────────── */ +type Key = { p: number; pos: [number, number, number]; look: [number, number, number] }; +const KEYS: Key[] = [ + { p: 0.0, pos: [0, 9, 20], look: [0, 0.8, 0] }, // wide — framed on BOTH districts' content (instant contrast) + { p: 0.28, pos: [-13, 3.4, 8.5], look: [-8, 1.0, 0] }, // road-level, chaotic left + { p: 0.5, pos: [0, 6.5, 9], look: [0, 2.2, -1] }, // transformation divide + { p: 0.72, pos: [13, 3.4, 8.5], look: [8, 1.0, 0] }, // road-level, optimized right + { p: 1.0, pos: [0, 15, 12], look: [0, 0.2, 4] }, // top-down logistics overview +]; +function sampleKeys(e: number) { + let a = KEYS[0], b = KEYS[KEYS.length - 1]; + for (let i = 0; i < KEYS.length - 1; i++) { + if (e >= KEYS[i].p && e <= KEYS[i + 1].p) { a = KEYS[i]; b = KEYS[i + 1]; break; } + } + const k = smoothstep(0, 1, (e - a.p) / ((b.p - a.p) || 1)); + return { + pos: [lerp(a.pos[0], b.pos[0], k), lerp(a.pos[1], b.pos[1], k), lerp(a.pos[2], b.pos[2], k)] as const, + look: [lerp(a.look[0], b.look[0], k), lerp(a.look[1], b.look[1], k), lerp(a.look[2], b.look[2], k)] as const, + }; +} +function CameraRig({ progress }: { progress: React.RefObject }) { + const eased = useRef(0); + const look = useRef(new THREE.Vector3(0, 1.5, 4)); + useFrame((state, dt) => { + eased.current = damp(eased.current, clamp01(progress.current ?? 0), 2.4, dt); + const { pos, look: lk } = sampleKeys(eased.current); + const t = state.clock.elapsedTime; + state.camera.position.set(pos[0] + Math.sin(t * 0.25) * 0.25, pos[1] + Math.sin(t * 0.4) * 0.12, pos[2]); + look.current.lerp(new THREE.Vector3(lk[0], lk[1], lk[2]), 0.12); + state.camera.lookAt(look.current); + }); + return null; +} + +/* ───────────── Roads (flat asphalt ribbons on the ground) ───────────── */ +const _p = new THREE.Vector3(), _t = new THREE.Vector3(); +function roadRibbon(curve: THREE.CatmullRomCurve3, width: number, segs = 130) { + const half = width / 2; + const pos: number[] = []; + const idx: number[] = []; + for (let i = 0; i <= segs; i++) { + const u = (i / segs) % 1; + curve.getPointAt(u, _p); + curve.getTangentAt(u, _t); + const nx = -_t.z, nz = _t.x; + const len = Math.hypot(nx, nz) || 1; + pos.push(_p.x + (nx / len) * half, ROUTE_Y, _p.z + (nz / len) * half); + pos.push(_p.x - (nx / len) * half, ROUTE_Y, _p.z - (nz / len) * half); + } + for (let i = 0; i < segs; i++) { + const a = i * 2, b = i * 2 + 1, c = i * 2 + 2, d = i * 2 + 3; + idx.push(a, b, c, b, d, c); + } + const g = new THREE.BufferGeometry(); + g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3)); + g.setIndex(idx); + g.computeVertexNormals(); + return g; +} +function Roads({ routes, edge, width }: { routes: { curve: THREE.CatmullRomCurve3 }[]; edge: string; width: number }) { + const geos = useMemo(() => routes.map((r) => roadRibbon(r.curve, width)), [routes, width]); + return ( + + {geos.map((g, i) => ( + + + + ))} + + ); +} + +/* Flowing fleet/data pulses riding the roads. */ +const PULSE_TMP = new THREE.Vector3(); +function RoadPulses({ routes, color, per = 3 }: { routes: { curve: THREE.CatmullRomCurve3 }[]; color: string; per?: number }) { + const count = routes.length * per; + const refs = useRef<(THREE.Mesh | null)[]>([]); + const offs = useMemo(() => Array.from({ length: count }, (_, i) => seeded(i * 7 + 3)), [count]); + useFrame((state) => { + const t = state.clock.elapsedTime; + for (let i = 0; i < count; i++) { + const m = refs.current[i]; + if (!m) continue; + const ri = Math.floor(i / per); + const u = (offs[i] + t * (0.04 + seeded(i) * 0.03)) % 1; + routes[ri].curve.getPointAt(u, PULSE_TMP); + m.position.set(PULSE_TMP.x, ROUTE_Y + 0.12, PULSE_TMP.z); + (m.material as THREE.MeshBasicMaterial).opacity = 0.6 + Math.sin(t * 6 + i) * 0.35; + } + }); + return ( + + {Array.from({ length: count }, (_, i) => ( + { refs.current[i] = el; }}> + + + + ))} + + ); +} + +/* ───────────── City buildings ───────────── */ +type Bld = { x: number; z: number; w: number; d: number; h: number; side: number; tone: number; accent: boolean }; +function Buildings() { + const blds = useMemo(() => { + const arr: Bld[] = []; + for (let gx = -15; gx <= 15; gx += 2.6) { + for (let gz = -12.5; gz <= 12.5; gz += 2.6) { + if (Math.abs(gx) < 1.8) continue; // keep gateway band clear + const center = gx < 0 ? LEFT_C : RIGHT_C; + const dCenter = Math.hypot(gx - center.x, gz - center.z); + if (dCenter < 6.4) continue; // keep road area clear + if (gz > 7.2 && Math.abs(gx) < 6.5) continue; // keep KPI tower area clear + if (seeded(gx * 7.3 + gz * 1.7 + 50) < 0.28) continue; // streets / gaps + const jx = (seeded(gx + gz) - 0.5) * 0.6; + const jz = (seeded(gx * 2 + gz) - 0.5) * 0.6; + arr.push({ + x: gx + jx, z: gz + jz, + w: 1.2 + seeded(gx * 1.1 + gz) * 0.8, + d: 1.2 + seeded(gx + gz * 1.3) * 0.8, + h: 1.1 + seeded(gx * 3 + gz * 5) * 3.6, + side: gx < 0 ? -1 : 1, + tone: seeded(gx * 9 + gz * 4), + accent: seeded(gx * 4 + gz * 11) > 0.62, + }); + } + } + return arr; + }, []); + + return ( + + {blds.map((b, i) => { + const bodyColor = b.tone < 0.4 ? C.bldA : b.tone < 0.75 ? C.bldB : (b.side > 0 ? C.white : C.bldC); + const accent = b.side < 0 ? (b.tone > 0.5 ? C.orange : C.red) : C.green; + return ( + + + + + + {b.accent && ( + + + + + )} + + ); + })} + + ); +} + +/* ───────────── Warehouse / dispatch depot ───────────── */ +function Warehouse({ x, z, accent }: { x: number; z: number; accent: string }) { + return ( + + + + + + {/* sloped roof slab */} + + + + + {/* roller doors */} + {[-1.3, 0, 1.3].map((o, i) => ( + + + + + ))} + + ); +} + +/* ───────────── Ground delivery zones + congestion heat ───────────── */ +function GroundMarks() { + const heat = useRef<(THREE.Mesh | null)[]>([]); + useFrame((state) => { + const t = state.clock.elapsedTime; + heat.current.forEach((m, i) => { + if (m) (m.material as THREE.MeshBasicMaterial).opacity = 0.18 + Math.sin(t * 2 + i) * 0.1; + }); + }); + const leftZones = useMemo(() => Array.from({ length: 4 }, (_, i) => { + const a = (i / 4) * Math.PI * 2 + 0.5; + return [LEFT_C.x + Math.cos(a) * 4.4, LEFT_C.z + Math.sin(a) * 4.4] as [number, number]; + }), []); + const rightZones = useMemo(() => Array.from({ length: 4 }, (_, i) => { + const a = (i / 4) * Math.PI * 2 + 0.5; + return [RIGHT_C.x + Math.cos(a) * 4.4, RIGHT_C.z + Math.sin(a) * 4.4] as [number, number]; + }), []); + return ( + + {/* left congestion heat patches (red/orange) */} + {leftZones.map(([x, z], i) => ( + { heat.current[i] = el; }} rotation={[-Math.PI / 2, 0, 0]} position={[x, 0.04, z]}> + + + + ))} + {/* right optimized coverage zones (green rings) */} + {rightZones.map(([x, z], i) => ( + + + + + ))} + + ); +} + +/* ───────────── Central transformation divider (NOT an AI engine) ───────────── + A clean glowing seam between the two worlds + a light "optimization wave" that + sweeps left→right, literally showing the chaotic side being transformed. */ +function TransformDivider() { + const sweep = useRef(null); + const seam = useRef(null); + useFrame((state) => { + const t = state.clock.elapsedTime; + if (sweep.current) { + const u = (t * 0.16) % 1; + sweep.current.position.x = lerp(-6, 6, u); + (sweep.current.material as THREE.MeshBasicMaterial).opacity = Math.sin(u * Math.PI) * 0.3; + } + if (seam.current) seam.current.opacity = 0.4 + Math.sin(t * 2) * 0.12; + }); + return ( + + {/* ground seam marking before | after */} + + + + + {/* vertical seam light */} + + + + + {/* sweeping optimization wave crossing the city */} + + + + + + ); +} + +/* Big number that counts up once on mount (drei Html children are normal DOM). */ +function CountUp({ value, decimals = 0, suffix = "" }: { value: number; decimals?: number; suffix?: string }) { + const [n, setN] = React.useState(0); + React.useEffect(() => { + let raf = 0; + let start: number | null = null; + const dur = 1700; + const tick = (now: number) => { + if (start === null) start = now; + const k = Math.min(1, (now - start) / dur); + setN(value * (1 - Math.pow(1 - k, 3))); + if (k < 1) raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [value]); + return <>{n.toFixed(decimals)}{suffix}; +} + +/* ───────────── KPI performance towers (grow with scroll) + BIG counting numbers ───────────── */ +type Kpi = { name: string; value: number; decimals?: number; suffix: string; h: number; x: number }; +const KPIS: Kpi[] = [ + { name: "Faster Deliveries", value: 32, suffix: "%", h: 5.4, x: -5.1 }, + { name: "Lower Op. Cost", value: 18, suffix: "%", h: 4.4, x: -1.7 }, + { name: "SLA Success", value: 99.2, decimals: 1, suffix: "%", h: 5.7, x: 1.7 }, + { name: "Less Fuel Used", value: 24, suffix: "%", h: 4.0, x: 5.1 }, +]; +function KpiTowers({ progress }: { progress: React.RefObject }) { + const refs = useRef<(THREE.Group | null)[]>([]); + const Z = 12.5; + useFrame(() => { + const p = clamp01(progress.current ?? 0); + KPIS.forEach((_, i) => { + const g = refs.current[i]; + if (!g) return; + const k = smoothstep(0.12 + i * 0.07, 0.55 + i * 0.07, p); + g.scale.y = 0.02 + k * 0.98; + }); + }); + return ( + + {KPIS.map((kpi, i) => ( + + + + + + { refs.current[i] = el; }}> + + + + + + + + + + {/* BIG, immediately-readable counting readout integrated on the tower */} + +
+
+ +
+
{kpi.name}
+
+ +
+ ))} +
+ ); +} + +/* ───────────── Fleet (spline-locked, drives on the roads) ───────────── */ +const TYPES: VType[] = ["truck", "van", "auto", "bike"]; +function Fleets() { + const { chaotic, optimized } = useMemo(() => buildPerfRoutes(), []); + const badBodies = [C.steel, "#7f1d1d", "#9a3412"]; + const goodBodies = [C.white, C.bldC, "#166534"]; + return ( + + {chaotic.map((r, i) => ( + + ))} + {optimized.map((r, i) => ( + + ))} + + ); +} + +/* ───────────── Scene ───────────── */ +function Scene({ progress, reduced, isMobile }: { progress: React.RefObject; reduced: boolean; isMobile: boolean }) { + const { chaotic, optimized } = useMemo(() => buildPerfRoutes(), []); + return ( + <> + + + + + + + {/* ground + faint street grid */} + + + + + + + {/* bold per-side atmosphere wash — instant "chaos vs efficiency" read */} + + + + + + + + + + + + {!isMobile && } + {!isMobile && } + + + + + + + + + + + {!reduced && ( + + {/* light bloom — only bright emissive (lights, pulses, gateway, tower caps) glow */} + + + + )} + + ); +} + +function PerformanceCanvas({ progress, reduced = false, isMobile = false, active = true }: Props) { + return ( + + + + + ); +} + +export default React.memo(PerformanceCanvas); diff --git a/src/components/performance/PerformanceSection.tsx b/src/components/performance/PerformanceSection.tsx new file mode 100644 index 0000000..3584b71 --- /dev/null +++ b/src/components/performance/PerformanceSection.tsx @@ -0,0 +1,189 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import dynamic from "next/dynamic"; +import { motion, useMotionValue, useTransform } from "framer-motion"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +const PerformanceCanvas = dynamic(() => import("./PerformanceCanvas"), { ssr: false }); + +export default function PerformanceSection() { + const containerRef = useRef(null); + const progressRef = useRef(0); + const scroll = useMotionValue(0); + + const [pinState, setPinState] = useState<"before" | "pinned" | "after">("before"); + const [mountScene, setMountScene] = useState(false); + const [sceneActive, setSceneActive] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [reduced, setReduced] = useState(false); + + useEffect(() => { + const mqMobile = window.matchMedia("(max-width: 767px)"); + const mqReduce = window.matchMedia("(prefers-reduced-motion: reduce)"); + const sync = () => { setIsMobile(mqMobile.matches); setReduced(mqReduce.matches); }; + sync(); + mqMobile.addEventListener("change", sync); + mqReduce.addEventListener("change", sync); + return () => { mqMobile.removeEventListener("change", sync); mqReduce.removeEventListener("change", sync); }; + }, []); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const mountIo = new IntersectionObserver( + (entries) => { + if (entries.some((e) => e.isIntersecting)) { + setMountScene(true); + setSceneActive(true); + mountIo.disconnect(); + } + }, + { rootMargin: "120% 0px" }, + ); + const activeIo = new IntersectionObserver( + (entries) => setSceneActive(entries.some((e) => e.isIntersecting)), + { rootMargin: "10% 0px" }, + ); + mountIo.observe(el); + activeIo.observe(el); + return () => { mountIo.disconnect(); activeIo.disconnect(); }; + }, []); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + gsap.registerPlugin(ScrollTrigger); + let lastPin: "before" | "pinned" | "after" = "before"; + const st = ScrollTrigger.create({ + trigger: el, + start: "top top", + end: "bottom bottom", + scrub: 0.4, + invalidateOnRefresh: true, + onUpdate: (self) => { + const p = self.progress; + progressRef.current = p; + scroll.set(p); + const ns = p <= 0.0002 ? "before" : p >= 0.9998 ? "after" : "pinned"; + if (ns !== lastPin) { lastPin = ns; setPinState(ns); } + }, + }); + const refresh = setTimeout(() => ScrollTrigger.refresh(), 300); + return () => { clearTimeout(refresh); st.kill(); }; + }, [scroll]); + + const beforeOpacity = useTransform(scroll, [0.1, 0.3, 0.46], [0.4, 1, 0.32]); + const afterOpacity = useTransform(scroll, [0.6, 0.74, 0.95], [0.32, 1, 0.7]); + const stageA = useTransform(scroll, [0, 0.4], [1, 0]); + const stageB = useTransform(scroll, [0.4, 0.55, 0.65], [0, 1, 0]); + const stageC = useTransform(scroll, [0.65, 0.85], [0, 1]); + + return ( +
+
+
+
+ {mountScene && ( +
+ +
+ )} +
+ +
+
+ + Results & Impact + + + What MileTruth Delivers + + + From congested traditional dispatch to a lean optimized fleet — the measurable business results across a live delivery city. + + +
+ Traditional dispatch + Transformation gateway + Optimized network +
+
+ + + Traditional Dispatch + Congestion · long routes · fuel waste · delays + + + MileTruth Optimized + Clean corridors · organized fleet · faster coverage + +
+
+
+ +
+ ); +} + +const styles = ` +.dm-perf { position: relative; height: 250vh; background: transparent; margin-bottom: 120px; } +.dm-perf-sticky { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; overflow: hidden; } +.dm-perf.is-pinned .dm-perf-sticky { position: fixed; top: 0; left: 0; } +.dm-perf.is-after .dm-perf-sticky { position: absolute; top: auto; bottom: 0; } + +.dm-perf-card { + position: absolute !important; top: 110px !important; left: 40px !important; right: 40px !important; bottom: 24px !important; + border-radius: 60px !important; overflow: hidden !important; + background: linear-gradient(168deg, #1b1f26 0%, #15181d 45%, #101216 100%) !important; + border: 1.5px solid rgba(255,255,255,0.08) !important; + box-shadow: 0 4px 30px -4px rgba(0,0,0,0.7), 0 20px 80px -20px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.05) !important; + box-sizing: border-box !important; +} +@media (max-width: 1024px) { .dm-perf-card { top: 96px !important; left: 20px !important; right: 20px !important; bottom: 16px !important; border-radius: 42px !important; } } +@media (max-width: 767px) { .dm-perf-card { top: 86px !important; left: 10px !important; right: 10px !important; bottom: 10px !important; border-radius: 28px !important; } } + +.dm-perf-backdrop { position: absolute; inset: 0; z-index: 0; + background: radial-gradient(55% 50% at 20% 60%, rgba(239,68,68,0.07) 0%, transparent 60%), + radial-gradient(55% 50% at 80% 60%, rgba(34,197,94,0.08) 0%, transparent 60%); } +.dm-perf-canvas { position: absolute; inset: 0; z-index: 1; } +.dm-perf-canvas canvas { display: block; } +.dm-perf-vignette { position: absolute; inset: 0; z-index: 2; pointer-events: none; + background: radial-gradient(125% 105% at 50% 46%, transparent 56%, rgba(8,9,12,0.86) 100%), + linear-gradient(180deg, rgba(8,9,12,0.5) 0%, transparent 20%, transparent 66%, rgba(8,9,12,0.9) 100%); } + +.dm-perf-ui { position: absolute; inset: 0; z-index: 4; pointer-events: none; + font-family: var(--font-space-grotesk), var(--font-manrope), system-ui, sans-serif; } + +.dm-perf-head { position: absolute; top: clamp(18px, 3.4vh, 40px); left: 50%; transform: translateX(-50%); width: min(700px, 92vw); text-align: center; } +.dm-perf-eyebrow { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; letter-spacing: 0.24em; text-transform: uppercase; color: #4ade80; + padding: 5px 14px; border-radius: 999px; background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.28); backdrop-filter: blur(8px); } +.dm-perf-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 10px #22c55e; } +.dm-perf .dm-perf-head h2 { margin: 10px 0 6px !important; padding: 0 !important; color: #F8FAFC !important; font-weight: 700 !important; text-transform: none !important; + font-size: clamp(22px, 2.6vw, 38px) !important; line-height: 1.08 !important; letter-spacing: -0.015em !important; } +.dm-perf .dm-perf-head p { margin: 0 auto !important; padding: 0 !important; color: rgba(226,232,240,0.66) !important; max-width: 500px; font-size: clamp(11px, 1vw, 13.5px) !important; line-height: 1.45 !important; } + +.dm-perf-status { display: inline-flex; align-items: center; gap: 16px; margin-top: 12px; min-height: 18px; } +.dm-perf-status__item { position: relative; display: inline-flex; align-items: center; gap: 7px; font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase; color: #E2E8F0; font-weight: 600; } +.dm-perf-status__item:not(:first-child) { position: absolute; left: 50%; transform: translateX(-50%); white-space: nowrap; } +.dm-perf-status__dot { width: 6px; height: 6px; border-radius: 50%; } +.dm-perf-status__dot--red { background: #ef4444; box-shadow: 0 0 10px #ef4444; } +.dm-perf-status__dot--amber { background: #fbbf24; box-shadow: 0 0 10px #fbbf24; } +.dm-perf-status__dot--green { background: #22c55e; box-shadow: 0 0 10px #22c55e; } + +.dm-perf-label { position: absolute; top: 44%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 4px; } +.dm-perf-label--before { left: clamp(16px, 4vw, 60px); text-align: left; } +.dm-perf-label--after { right: clamp(16px, 4vw, 60px); text-align: right; align-items: flex-end; } +.dm-perf-label__tag { font-size: clamp(17px, 2vw, 28px); font-weight: 800; letter-spacing: -0.02em; color: #f87171; text-shadow: 0 0 22px rgba(239,68,68,0.45); } +.dm-perf-label__tag--good { color: #4ade80; text-shadow: 0 0 22px rgba(34,197,94,0.5); } +.dm-perf-label__sub { font-size: 10.5px; letter-spacing: 0.05em; color: rgba(226,232,240,0.6); max-width: 180px; } + +@media (max-width: 767px) { + .dm-perf { height: 220vh; } + .dm-perf-label__sub { display: none; } + .dm-perf-label__tag { font-size: 15px; } + .dm-perf-head h2 { font-size: 22px; } + .dm-perf-status { gap: 10px; } +} +`; diff --git a/src/components/performance/Vehicles.tsx b/src/components/performance/Vehicles.tsx new file mode 100644 index 0000000..f1a7ba0 --- /dev/null +++ b/src/components/performance/Vehicles.tsx @@ -0,0 +1,183 @@ +"use client"; + +import React, { useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; + +export type VType = "bike" | "auto" | "van" | "truck"; + +const TMP_POS = new THREE.Vector3(); +const TMP_TAN = new THREE.Vector3(); + +/* ── Low-poly procedural vehicle bodies (modelled facing +Z) ────────────── + Body uses a lit standard material; accent strips + lights are emissive so + they pick up bloom. Wheels are a shared group that spins. */ + +function Wheels({ positions, radius = 0.12, spinRef }: { + positions: [number, number, number][]; + radius?: number; + spinRef: React.RefObject; +}) { + return ( + + {positions.map((p, i) => ( + + + + + ))} + + ); +} + +function VehicleBody({ type, body, accent }: { type: VType; body: string; accent: string }) { + const wheels = useRef(null); + // spin the wheels for a sense of motion + useFrame((_, dt) => { + if (wheels.current) wheels.current.rotation.x += dt * 9; + }); + + const std = (color: string, e = 0.15) => ( + + ); + const glow = (color: string) => ( + + ); + + if (type === "bike") { + return ( + + + + {std(body, 0.2)} + + + + {std("#1e293b", 0.1)} + + + + {glow(accent)} + + + + {glow("#ffffff")} + + + + ); + } + + if (type === "auto") { + // three-wheeled auto-rickshaw + return ( + + + + {std(body, 0.18)} + + + + {std("#0b1220", 0.05)} + + + + {glow(accent)} + + {glow("#ffffff")} + {glow("#ffffff")} + + + ); + } + + if (type === "van") { + return ( + + + + {std(body, 0.16)} + + + + {std("#1e293b", 0.05)} + + + + {glow(accent)} + + {glow("#ffffff")} + {glow("#ffffff")} + + + ); + } + + // truck — cab + long box trailer + return ( + + + + {std(body, 0.18)} + + + + {std("#cbd5e1", 0.08)} + + + + {glow(accent)} + + {glow("#ffffff")} + {glow("#ffffff")} + + + ); +} + +export const VehicleBodyMemo = React.memo(VehicleBody); + +/** + * Locks a vehicle perfectly to a spline. Position = curve.getPointAt(u), + * heading = curve.getTangentAt(u). No drifting, no off-route movement. + */ +export function SplineRider({ + curve, + speed, + offset, + type, + body, + accent, +}: { + curve: THREE.CatmullRomCurve3; + speed: number; + offset: number; + type: VType; + body: string; + accent: string; +}) { + const ref = useRef(null); + const u = useRef(offset); + + useFrame((_, dt) => { + const g = ref.current; + if (!g) return; + u.current = (u.current + dt * speed) % 1; + curve.getPointAt(u.current, TMP_POS); + curve.getTangentAt(u.current, TMP_TAN); + g.position.copy(TMP_POS); + g.rotation.y = Math.atan2(TMP_TAN.x, TMP_TAN.z); + // gentle pitch so it follows slope without leaving the path + g.rotation.x = -Math.asin(THREE.MathUtils.clamp(TMP_TAN.y, -0.6, 0.6)) * 0.5; + }); + + return ( + + + + ); +} diff --git a/src/components/performance/perfRoutes.ts b/src/components/performance/perfRoutes.ts new file mode 100644 index 0000000..b95c0a3 --- /dev/null +++ b/src/components/performance/perfRoutes.ts @@ -0,0 +1,69 @@ +import * as THREE from "three"; +import { seeded } from "../optimization/math"; + +/** + * Ground-level ROAD network for the Performance "Results & Impact" city. + * - LEFT district (x < 0): traditional dispatch — tangled, overlapping roads. + * - RIGHT district (x > 0): MileTruth optimized — clean, organized corridors. + * All curves are CLOSED and flat on the ground so fleet vehicles drive on the + * roads continuously (spline-locked, no end-of-curve teleport). + */ + +export const ROUTE_Y = 0.05; // roads sit on the ground +export const LEFT_C = new THREE.Vector3(-8, 0, 0); +export const RIGHT_C = new THREE.Vector3(8, 0, 0); +export const GATEWAY = new THREE.Vector3(0, 0, 0); // central transformation gateway + +export type PerfRoute = { curve: THREE.CatmullRomCurve3 }; + +function closedFlat(points: THREE.Vector3[]): THREE.CatmullRomCurve3 { + const flat = points.map((p) => new THREE.Vector3(p.x, ROUTE_Y, p.z)); + return new THREE.CatmullRomCurve3(flat, true, "catmullrom", 0.5); +} + +let cache: { chaotic: PerfRoute[]; optimized: PerfRoute[] } | null = null; + +export function buildPerfRoutes() { + if (cache) return cache; + + // --- LEFT: tangled, overlapping traffic loops ---------------------------- + const chaotic: PerfRoute[] = []; + for (let r = 0; r < 5; r++) { + const pts: THREE.Vector3[] = []; + const n = 6 + Math.floor(seeded(r * 13 + 1) * 3); + let ang = seeded(r * 13 + 2) * Math.PI * 2; + for (let s = 0; s < n; s++) { + ang += (seeded(r * 13 + s * 3 + 3) - 0.5) * 2.7; // erratic detours + const rad = 1.8 + seeded(r * 7 + s + 4) * 4.2; + const x = LEFT_C.x + Math.cos(ang) * rad + (seeded(r * 5 + s) - 0.5) * 2.2; + const z = LEFT_C.z + Math.sin(ang) * rad + (seeded(r * 9 + s) - 0.5) * 2.2; + pts.push(new THREE.Vector3(x, 0, z)); + } + chaotic.push({ curve: closedFlat(pts) }); + } + + // --- RIGHT: clean organized delivery corridors, one per zone ------------- + const optimized: PerfRoute[] = []; + const zones: [number, number][] = [ + [0, 0], + [2.8, 2.6], + [-2.8, 2.6], + [2.8, -2.6], + [-2.8, -2.6], + ]; + zones.forEach(([ox, oz], r) => { + const pts: THREE.Vector3[] = []; + const n = 6; + const rad = r === 0 ? 4.8 : 1.5; + for (let s = 0; s < n; s++) { + const a = (s / n) * Math.PI * 2 + r * 0.3; + const x = RIGHT_C.x + ox + Math.cos(a) * rad; + const z = RIGHT_C.z + oz + Math.sin(a) * rad; + pts.push(new THREE.Vector3(x, 0, z)); + } + optimized.push({ curve: closedFlat(pts) }); + }); + + cache = { chaotic, optimized }; + return cache; +} diff --git a/src/components/sections/BlogGrid.tsx b/src/components/sections/BlogGrid.tsx index 389977b..a06bf29 100644 --- a/src/components/sections/BlogGrid.tsx +++ b/src/components/sections/BlogGrid.tsx @@ -158,10 +158,6 @@ export default function BlogGrid() { color: #64748b !important; line-height: 1.6 !important; margin: 0 0 24px 0 !important; - display: -webkit-box !important; - -webkit-line-clamp: 3 !important; - -webkit-box-orient: vertical !important; - overflow: hidden !important; text-transform: none !important; font-family: var(--font-manrope), sans-serif !important; } diff --git a/src/components/sections/BlogsHero.tsx b/src/components/sections/BlogsHero.tsx index 8c194d4..cfb22b8 100644 --- a/src/components/sections/BlogsHero.tsx +++ b/src/components/sections/BlogsHero.tsx @@ -4,6 +4,17 @@ import Link from "next/link"; export default function BlogsHero() { return ( <> +