diff --git a/package-lock.json b/package-lock.json index ee9d168..146f2be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@designcodeio/threeui", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@designcodeio/threeui", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "three128": "npm:three@0.128.0", diff --git a/package.json b/package.json index c7114f3..a2fdca0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@designcodeio/threeui", - "version": "1.1.0", + "version": "1.2.0", "description": "Open-source React components and interactive templates from ThreeUI Community.", "license": "MIT", "type": "module", diff --git a/public/community-sync-report.json b/public/community-sync-report.json index e2d7e56..cfb00ed 100644 --- a/public/community-sync-report.json +++ b/public/community-sync-report.json @@ -1,14 +1,14 @@ { "schemaVersion": 1, - "generatedAt": "2026-08-25T10:26:00.704Z", - "sourceCommit": "bab60dad20e2290cf8e35753bf4fec14c017567e", + "generatedAt": "2026-08-31T16:05:46.996Z", + "sourceCommit": "6b65f2dbec22ac58c56474f028aec5590e62d3c9", "communityParents": 43, "communityRoutes": 104, - "communityVariants": 147, - "excludedProParents": 86, + "communityVariants": 163, + "excludedProParents": 108, "excludedBetaParents": 23, - "layoutStylesSha256": "5400401f66eb773d05a7b22a6e2fd16651b732a5bd366eee1ab8b479e0327f0d", - "rendererStylesSha256": "6d975e7d3332aa85d7bc1a55cdff592d63b0403f0988dfefa905dbe4c341d328", + "layoutStylesSha256": "cee8ebe47ad857149f52f12bb12324a83856fc403ea1431f238aec4ecc385760", + "rendererStylesSha256": "dc36aeb914df4c554164304292d75738a770e454b1e2710bac1ce4169d5fee2a", "components": [ { "id": "kage-landing-page", @@ -136,7 +136,12 @@ }, { "id": "crt", - "variantIds": [], + "variantIds": [ + "terminal", + "cinematic", + "blue-screen", + "nintendo" + ], "controlKeys": [ "speed", "typeSpeed", @@ -146,7 +151,41 @@ "brightness", "opacity" ], - "variantControlKeys": {} + "variantControlKeys": { + "terminal": [ + "speed", + "typeSpeed", + "motion", + "hue", + "saturation", + "brightness", + "opacity" + ], + "cinematic": [ + "speed", + "motion", + "hue", + "saturation", + "brightness", + "opacity" + ], + "blue-screen": [ + "speed", + "motion", + "hue", + "saturation", + "brightness", + "opacity" + ], + "nintendo": [ + "speed", + "motion", + "hue", + "saturation", + "brightness", + "opacity" + ] + } }, { "id": "energy-orb", @@ -769,13 +808,23 @@ }, { "id": "woven-cloth", - "variantIds": [], + "variantIds": [ + "woven-cloth", + "iridescent", + "atelier", + "washi" + ], "controlKeys": [ "hue", "saturation", "brightness" ], - "variantControlKeys": {} + "variantControlKeys": { + "woven-cloth": [], + "iridescent": [], + "atelier": [], + "washi": [] + } }, { "id": "performance-gauges", @@ -863,7 +912,12 @@ }, { "id": "animated-top-dock", - "variantIds": [], + "variantIds": [ + "sable", + "modern", + "retro", + "glass" + ], "controlKeys": [ "proximity", "spring", @@ -872,7 +926,44 @@ "heightGrowth", "drop" ], - "variantControlKeys": {} + "variantControlKeys": { + "sable": [ + "proximity", + "spring", + "damping", + "widthGrowth", + "heightGrowth", + "drop" + ], + "modern": [ + "proximity", + "spring", + "damping", + "widthGrowth", + "heightGrowth", + "drop" + ], + "retro": [ + "pixelSize", + "levels", + "noise", + "scanlines", + "speed", + "proximity", + "widthGrowth" + ], + "glass": [ + "particles", + "thickness", + "dispersion", + "specular", + "rim", + "drift", + "proximity", + "heightGrowth", + "drop" + ] + } }, { "id": "sketchbook", @@ -966,16 +1057,27 @@ }, { "id": "skeuomorphic-toggle", - "variantIds": [], + "variantIds": [ + "skeuomorphic-toggle", + "modern", + "glass", + "shader" + ], "controlKeys": [ "mode", + "speed", "size", "opacity", "hue", "saturation", "brightness" ], - "variantControlKeys": {} + "variantControlKeys": { + "skeuomorphic-toggle": [], + "modern": [], + "glass": [], + "shader": [] + } }, { "id": "matrix-field", diff --git a/public/source-code.json b/public/source-code.json index fb55ea7..2b54eee 100644 --- a/public/source-code.json +++ b/public/source-code.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "generatedAt": "2026-08-25T10:26:00.248Z", + "generatedAt": "2026-08-31T16:05:46.446Z", "readyIds": [ "kage-landing-page", "complete-shelf-landing-page", @@ -51,10 +51,10 @@ "path": "src/shaders/community.css", "language": "css", "role": "shared-style", - "bytes": 10733, - "lines": 549, - "sha256": "6d975e7d3332aa85d7bc1a55cdff592d63b0403f0988dfefa905dbe4c341d328", - "code": "/* Generated from the main renderer stylesheet; Community selectors only. */\n.threeui-mount,\n.text-path-study,\n.article-headings-component,\n.animated-top-dock-component,\n.typography-vortex-component,\n.landscape-scene,\n.japanese-tower-landscape,\n.liquid-metal-button,\n.spark-badge,\n.threeui-background {\n position: relative;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n\n.japanese-tower-landscape {\n isolation: isolate;\n background: #ecdcbc;\n}\n\n.japanese-tower-landscape__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #ecdcbc;\n opacity: 0;\n transition: opacity 180ms ease-out;\n}\n\n.japanese-tower-landscape__frame.is-ready {\n opacity: 1;\n}\n\n.landscape-scene {\n isolation: isolate;\n background: #ecdcbc;\n}\n\n.landscape-scene__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #ecdcbc;\n opacity: 0;\n transition: opacity 180ms ease-out;\n touch-action: none;\n}\n\n.landscape-scene__frame.is-ready {\n opacity: 1;\n}\n\n.liquid-metal-button {\n isolation: isolate;\n background: #070708;\n}\n\n.liquid-metal-button__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #070708;\n opacity: 0;\n transition: opacity 180ms ease-out;\n}\n\n.liquid-metal-button__frame.is-ready {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .liquid-metal-button__frame {\n transition: none;\n }\n}\n\n.spark-badge {\n isolation: isolate;\n background: #000;\n}\n\n.spark-badge__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #000;\n opacity: 0;\n transition: opacity 180ms ease-out;\n}\n\n.spark-badge__frame.is-ready {\n opacity: 1;\n}\n\n.text-path-study {\n isolation: isolate;\n background: #08090a;\n}\n\n.text-path-study--light {\n background: #f3f5f8;\n}\n\n.text-path-study-frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: transparent;\n transform-origin: 50% 50%;\n}\n\n.threeui-background {\n position: relative;\n isolation: isolate;\n background: #030304;\n}\n\n.threeui-background.condensation {\n background: transparent;\n}\n\n.threeui-background.predictive-arc--light,\n.threeui-background.data-pixel-arc--light {\n background: #f3f6f1;\n}\n\n.typography-vortex-component.typography-vortex-component--light {\n background: #eef1f6;\n}\n\n.typography-vortex-component--light .typography-vortex-component__hint {\n color: rgba(28, 30, 38, 0.55);\n}\n\n.threeui-background > canvas {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.temple-night-scene,\n.temple-night-canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.temple-night-scene {\n position: relative;\n overflow: hidden;\n background: #05070a;\n}\n\n.temple-night-canvas {\n position: relative;\n z-index: 1;\n opacity: 0;\n touch-action: none;\n}\n\n.temple-night-canvas.is-ready {\n opacity: 1;\n}\n\n.temple-night-unavailable {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n margin: 0;\n color: #a9b2b7;\n background: #05070a;\n}\n\n.bookshelf,\n.bookshelf__canvas,\n.sketchbook,\n.sketchbook__frame {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.bookshelf {\n position: relative;\n container-type: inline-size;\n overflow: hidden;\n background: #171a24;\n outline: none;\n}\n\n.bookshelf__canvas {\n position: absolute;\n inset: 0;\n opacity: 0;\n touch-action: none;\n}\n\n.bookshelf__canvas.is-ready {\n opacity: 1;\n}\n\n.bookshelf__source-controls {\n display: none;\n}\n\n.bookshelf__unavailable {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n margin: 0;\n padding: 24px;\n color: #f4eee6;\n text-align: center;\n}\n\n.sketchbook {\n position: relative;\n overflow: hidden;\n background: #ece7dc;\n}\n\n.sketchbook__frame {\n border: 0;\n opacity: 0;\n background: #ece7dc;\n}\n\n.sketchbook__frame.is-ready {\n opacity: 1;\n}\n\n@font-face {\n font-family: \"ThreeUI Fragment Mono\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"./fonts/fragment-mono.woff2\") format(\"woff2\");\n}\n\n.article-headings-component,\n.animated-top-dock-component,\n.typography-vortex-component {\n container-type: inline-size;\n color: #ecece8;\n background: #0b0b0b;\n font-family: \"Geist\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n\n.article-headings-component {\n display: grid;\n grid-template-rows: auto 1fr 1fr;\n padding: clamp(20px, 5.4cqw, 74px) clamp(20px, 6.2cqw, 86px);\n isolation: isolate;\n}\n\n.article-headings-component::after,\n.animated-top-dock-component::after,\n.typography-vortex-component::after {\n position: absolute;\n inset: 0;\n z-index: 5;\n pointer-events: none;\n content: \"\";\n opacity: 0.12;\n background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E\");\n mix-blend-mode: soft-light;\n}\n\n.article-headings-component__header {\n display: flex;\n justify-content: space-between;\n padding-bottom: clamp(14px, 2.6cqw, 30px);\n color: #62625e;\n font: 400 clamp(8px, 0.85cqw, 10px) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.12em;\n}\n\n.article-headings-component__entry {\n display: grid;\n grid-template-columns: clamp(28px, 4cqw, 56px) minmax(0, 1fr) auto;\n align-items: center;\n gap: clamp(10px, 2.2cqw, 30px);\n border-top: 1px solid #2a2a28;\n}\n\n.article-headings-component__entry h2 {\n margin: 0;\n color: #e9e9e5;\n font-size: clamp(18px, 3.2cqw, 44px);\n font-weight: 600;\n line-height: 1.08;\n letter-spacing: -0.02em;\n}\n\n.article-headings-component__index,\n.article-headings-component__meta {\n color: #60605c;\n font: 400 clamp(8px, 0.85cqw, 10px) / 1.3 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.06em;\n}\n\n.article-headings-component__meta {\n text-align: right;\n}\n\n.article-headings-component--light {\n color: #202128;\n background: #f3f5f8;\n}\n\n.article-headings-component--light .article-headings-component__header,\n.article-headings-component--light .article-headings-component__index,\n.article-headings-component--light .article-headings-component__meta {\n color: #71737b;\n}\n\n.article-headings-component--light .article-headings-component__entry {\n border-color: #d9dce3;\n}\n\n.article-headings-component--light .article-headings-component__entry h2 {\n color: #1d1f26;\n}\n\n.typography-vortex-component {\n position: relative;\n isolation: isolate;\n touch-action: none;\n cursor: crosshair;\n}\n\n.typography-vortex-component canvas {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.typography-vortex-component__hint,\n.animated-top-dock-component__caption {\n position: absolute;\n z-index: 6;\n color: #62625e;\n font: 400 9px/1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.1em;\n}\n\n.typography-vortex-component__hint {\n right: 18px;\n bottom: 16px;\n}\n\n.animated-top-dock-component {\n position: relative;\n display: grid;\n place-items: center;\n isolation: isolate;\n background:\n radial-gradient(circle at 50% 20%, rgba(255, 255, 255, 0.035), transparent 34%),\n #080808;\n}\n\n.animated-top-dock__nav {\n position: absolute;\n top: clamp(24px, 5cqw, 46px);\n left: 50%;\n z-index: 10;\n display: flex;\n align-items: flex-start;\n gap: 3px;\n height: 38px;\n padding: 4px;\n overflow: visible;\n border: 1px solid #292929;\n border-radius: 11px;\n background: rgba(14, 14, 14, 0.86);\n box-shadow: 0 12px 34px rgba(0, 0, 0, 0.48), inset 0 1px rgba(255, 255, 255, 0.035);\n backdrop-filter: blur(18px) saturate(75%);\n transform: translateX(-50%);\n isolation: isolate;\n}\n\n.animated-top-dock__item {\n position: relative;\n z-index: 6;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n height: 28px;\n min-width: 0;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 7px;\n outline: none;\n color: #858580;\n background: rgba(21, 21, 20, 0.9);\n font: 400 10px/1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.1em;\n white-space: nowrap;\n backdrop-filter: blur(14px) saturate(70%);\n transform-origin: 50% 0;\n transition: color 0.15s, border-color 0.18s, background 0.18s, box-shadow 0.18s;\n will-change: width, height, transform;\n cursor: pointer;\n}\n\n.animated-top-dock__item[data-dock-near=\"true\"],\n.animated-top-dock__item:focus-visible {\n z-index: 7;\n color: #ecece8;\n border-color: #41413e;\n background: rgba(27, 27, 26, 0.94);\n box-shadow: 0 7px 16px rgba(0, 0, 0, 0.26);\n}\n\n.animated-top-dock__logo {\n width: 28px;\n height: 28px;\n overflow: hidden;\n background: #e8e8e3;\n}\n\n.animated-top-dock__logo svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.animated-top-dock__link {\n gap: 6px;\n width: 94px;\n padding: 0 9px;\n}\n\n.animated-top-dock__link[aria-pressed=\"true\"] {\n color: #111;\n border-color: #e8e8e3;\n background: #e8e8e3;\n box-shadow: 0 7px 18px rgba(0, 0, 0, 0.28);\n}\n\n.animated-top-dock__icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 13px;\n height: 13px;\n opacity: 0.62;\n transition: opacity 0.16s;\n}\n\n.animated-top-dock__icon svg {\n display: block;\n width: 100%;\n height: 100%;\n fill: none;\n stroke: currentColor;\n stroke-width: 1.2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.animated-top-dock__item[data-dock-near=\"true\"] .animated-top-dock__icon,\n.animated-top-dock__item:focus-visible .animated-top-dock__icon {\n opacity: 1;\n}\n\n.animated-top-dock-component__caption {\n right: 18px;\n bottom: 16px;\n}\n\n@media (max-width: 600px) {\n .article-headings-component {\n padding: 18px;\n }\n\n .article-headings-component__entry {\n grid-template-columns: 24px minmax(0, 1fr);\n }\n\n .article-headings-component__entry h2 {\n font-size: clamp(17px, 5.5vw, 26px);\n }\n\n .article-headings-component__meta {\n display: none;\n }\n\n .animated-top-dock__nav {\n top: 24px;\n gap: 2px;\n height: 36px;\n }\n\n .animated-top-dock__item {\n height: 26px !important;\n transform: none !important;\n }\n\n .animated-top-dock__logo {\n width: 26px !important;\n height: 26px !important;\n }\n\n .animated-top-dock__link {\n width: auto !important;\n padding: 0 7px;\n font-size: 8px;\n letter-spacing: 0.07em;\n }\n\n .animated-top-dock__icon {\n display: none;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .animated-top-dock__item {\n transform: none !important;\n }\n}\n" + "bytes": 31063, + "lines": 1272, + "sha256": "dc36aeb914df4c554164304292d75738a770e454b1e2710bac1ce4169d5fee2a", + "code": "/* Generated from the main renderer stylesheet; Community selectors only. */\n.threeui-mount,\n.text-path-study,\n.article-headings-component,\n.animated-top-dock-component,\n.typography-vortex-component,\n.landscape-scene,\n.japanese-tower-landscape,\n.liquid-metal-button,\n.spark-badge,\n.threeui-background {\n position: relative;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n\n.japanese-tower-landscape {\n isolation: isolate;\n background: #ecdcbc;\n}\n\n.japanese-tower-landscape__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #ecdcbc;\n opacity: 0;\n transition: opacity 180ms ease-out;\n}\n\n.japanese-tower-landscape__frame.is-ready {\n opacity: 1;\n}\n\n.landscape-scene {\n isolation: isolate;\n background: #ecdcbc;\n}\n\n.landscape-scene__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #ecdcbc;\n opacity: 0;\n transition: opacity 180ms ease-out;\n touch-action: none;\n}\n\n.landscape-scene__frame.is-ready {\n opacity: 1;\n}\n\n.liquid-metal-button {\n isolation: isolate;\n background: #070708;\n}\n\n.liquid-metal-button__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #070708;\n opacity: 0;\n transition: opacity 180ms ease-out;\n}\n\n.liquid-metal-button__frame.is-ready {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .liquid-metal-button__frame {\n transition: none;\n }\n}\n\n.spark-badge {\n isolation: isolate;\n background: #000;\n}\n\n.spark-badge__frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: #000;\n opacity: 0;\n transition: opacity 180ms ease-out;\n}\n\n.spark-badge__frame.is-ready {\n opacity: 1;\n}\n\n.text-path-study {\n isolation: isolate;\n background: #08090a;\n}\n\n.text-path-study--light {\n background: #f3f5f8;\n}\n\n.text-path-study-frame {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n background: transparent;\n transform-origin: 50% 50%;\n}\n\n.threeui-background {\n position: relative;\n isolation: isolate;\n background: #030304;\n}\n\n.threeui-background.condensation {\n background: transparent;\n}\n\n.threeui-background.predictive-arc--light,\n.threeui-background.data-pixel-arc--light {\n background: #f3f6f1;\n}\n\n.typography-vortex-component.typography-vortex-component--light {\n background: #eef1f6;\n}\n\n.typography-vortex-component--light .typography-vortex-component__hint {\n color: rgba(28, 30, 38, 0.55);\n}\n\n.threeui-background > canvas {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.temple-night-scene,\n.temple-night-canvas {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.temple-night-scene {\n position: relative;\n overflow: hidden;\n background: #05070a;\n}\n\n.temple-night-canvas {\n position: relative;\n z-index: 1;\n opacity: 0;\n touch-action: none;\n}\n\n.temple-night-canvas.is-ready {\n opacity: 1;\n}\n\n.temple-night-unavailable {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n margin: 0;\n color: #a9b2b7;\n background: #05070a;\n}\n\n.bookshelf,\n.bookshelf__canvas,\n.sketchbook,\n.sketchbook__frame {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.bookshelf {\n position: relative;\n container-type: inline-size;\n overflow: hidden;\n background: #171a24;\n outline: none;\n}\n\n.bookshelf__canvas {\n position: absolute;\n inset: 0;\n opacity: 0;\n touch-action: none;\n}\n\n.bookshelf__canvas.is-ready {\n opacity: 1;\n}\n\n.bookshelf__source-controls {\n display: none;\n}\n\n.bookshelf__unavailable {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n margin: 0;\n padding: 24px;\n color: #f4eee6;\n text-align: center;\n}\n\n.sketchbook {\n position: relative;\n overflow: hidden;\n background: #ece7dc;\n}\n\n.sketchbook__frame {\n border: 0;\n opacity: 0;\n background: #ece7dc;\n}\n\n.sketchbook__frame.is-ready {\n opacity: 1;\n}\n\n@font-face {\n font-family: \"ThreeUI Fragment Mono\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(\"./fonts/fragment-mono.woff2\") format(\"woff2\");\n}\n\n.article-headings-component,\n.animated-top-dock-component,\n.typography-vortex-component {\n container-type: inline-size;\n color: #ecece8;\n background: #0b0b0b;\n font-family: \"Geist\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n\n.article-headings-component {\n display: grid;\n grid-template-rows: auto 1fr 1fr;\n padding: clamp(20px, 5.4cqw, 74px) clamp(20px, 6.2cqw, 86px);\n isolation: isolate;\n}\n\n.article-headings-component::after,\n.animated-top-dock-component::after,\n.typography-vortex-component::after {\n position: absolute;\n inset: 0;\n z-index: 5;\n pointer-events: none;\n content: \"\";\n opacity: 0.12;\n background-image: url(\"data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E\");\n mix-blend-mode: soft-light;\n}\n\n.article-headings-component__header {\n display: flex;\n justify-content: space-between;\n padding-bottom: clamp(14px, 2.6cqw, 30px);\n color: #62625e;\n font: 400 clamp(8px, 0.85cqw, 10px) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.12em;\n}\n\n.article-headings-component__entry {\n display: grid;\n grid-template-columns: clamp(28px, 4cqw, 56px) minmax(0, 1fr) auto;\n align-items: center;\n gap: clamp(10px, 2.2cqw, 30px);\n border-top: 1px solid #2a2a28;\n}\n\n.article-headings-component__entry h2 {\n margin: 0;\n color: #e9e9e5;\n font-size: clamp(18px, 3.2cqw, 44px);\n font-weight: 600;\n line-height: 1.08;\n letter-spacing: -0.02em;\n}\n\n.article-headings-component__index,\n.article-headings-component__meta {\n color: #60605c;\n font: 400 clamp(8px, 0.85cqw, 10px) / 1.3 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.06em;\n}\n\n.article-headings-component__meta {\n text-align: right;\n}\n\n.article-headings-component--light {\n color: #202128;\n background: #f3f5f8;\n}\n\n.article-headings-component--light .article-headings-component__header,\n.article-headings-component--light .article-headings-component__index,\n.article-headings-component--light .article-headings-component__meta {\n color: #71737b;\n}\n\n.article-headings-component--light .article-headings-component__entry {\n border-color: #d9dce3;\n}\n\n.article-headings-component--light .article-headings-component__entry h2 {\n color: #1d1f26;\n}\n\n.typography-vortex-component {\n position: relative;\n isolation: isolate;\n touch-action: none;\n cursor: crosshair;\n}\n\n.typography-vortex-component canvas {\n position: absolute;\n inset: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.typography-vortex-component__hint,\n.animated-top-dock-component__caption {\n position: absolute;\n z-index: 6;\n color: #62625e;\n font: 400 9px/1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.1em;\n}\n\n.typography-vortex-component__hint {\n right: 18px;\n bottom: 16px;\n}\n\n.animated-top-dock-component {\n position: relative;\n display: grid;\n place-items: center;\n isolation: isolate;\n background:\n radial-gradient(circle at 50% 20%, rgba(255, 255, 255, 0.035), transparent 34%),\n #080808;\n}\n\n.animated-top-dock__nav {\n position: absolute;\n top: clamp(24px, 5cqw, 46px);\n left: 50%;\n z-index: 10;\n display: flex;\n align-items: flex-start;\n gap: 3px;\n height: 38px;\n padding: 4px;\n overflow: visible;\n border: 1px solid #292929;\n border-radius: 11px;\n background: rgba(14, 14, 14, 0.86);\n box-shadow: 0 12px 34px rgba(0, 0, 0, 0.48), inset 0 1px rgba(255, 255, 255, 0.035);\n backdrop-filter: blur(18px) saturate(75%);\n transform: translateX(-50%);\n isolation: isolate;\n}\n\n.animated-top-dock__item {\n position: relative;\n z-index: 6;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n height: 28px;\n min-width: 0;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 7px;\n outline: none;\n color: #858580;\n background: rgba(21, 21, 20, 0.9);\n font: 400 10px/1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.1em;\n white-space: nowrap;\n backdrop-filter: blur(14px) saturate(70%);\n transform-origin: 50% 0;\n transition: color 0.15s, border-color 0.18s, background 0.18s, box-shadow 0.18s;\n will-change: width, height, transform;\n cursor: pointer;\n}\n\n.animated-top-dock__item[data-dock-near=\"true\"],\n.animated-top-dock__item:focus-visible {\n z-index: 7;\n color: #ecece8;\n border-color: #41413e;\n background: rgba(27, 27, 26, 0.94);\n box-shadow: 0 7px 16px rgba(0, 0, 0, 0.26);\n}\n\n.animated-top-dock__logo {\n width: 28px;\n height: 28px;\n overflow: hidden;\n background: #e8e8e3;\n}\n\n.animated-top-dock__logo svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.animated-top-dock__link {\n gap: 6px;\n width: 94px;\n padding: 0 9px;\n}\n\n.animated-top-dock__link[aria-pressed=\"true\"] {\n color: #111;\n border-color: #e8e8e3;\n background: #e8e8e3;\n box-shadow: 0 7px 18px rgba(0, 0, 0, 0.28);\n}\n\n.animated-top-dock__icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 13px;\n height: 13px;\n opacity: 0.62;\n transition: opacity 0.16s;\n}\n\n.animated-top-dock__icon svg {\n display: block;\n width: 100%;\n height: 100%;\n fill: none;\n stroke: currentColor;\n stroke-width: 1.2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.animated-top-dock__item[data-dock-near=\"true\"] .animated-top-dock__icon,\n.animated-top-dock__item:focus-visible .animated-top-dock__icon {\n opacity: 1;\n}\n\n.animated-top-dock-component__caption {\n right: 18px;\n bottom: 16px;\n}\n\n@media (max-width: 600px) {\n .article-headings-component {\n padding: 18px;\n }\n\n .article-headings-component__entry {\n grid-template-columns: 24px minmax(0, 1fr);\n }\n\n .article-headings-component__entry h2 {\n font-size: clamp(17px, 5.5vw, 26px);\n }\n\n .article-headings-component__meta {\n display: none;\n }\n\n .animated-top-dock__nav {\n top: 24px;\n gap: 2px;\n height: 36px;\n }\n\n .animated-top-dock__item {\n height: 26px !important;\n transform: none !important;\n }\n\n .animated-top-dock__logo {\n width: 26px !important;\n height: 26px !important;\n }\n\n .animated-top-dock__link {\n width: auto !important;\n padding: 0 7px;\n font-size: 8px;\n letter-spacing: 0.07em;\n }\n\n .animated-top-dock__icon {\n display: none;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .animated-top-dock__item {\n transform: none !important;\n }\n}\n\n/* ── Top dock variants ────────────────────────────────────────────────────\n Every dimension below is a container query unit with a clamp, because these\n bars are shown at anything from a 600 px card to a 1920 px capture frame: a\n fixed 56 px bar that reads correctly in the catalogue frame becomes a thin\n strip of small type at full width. One scale block feeds all three. */\n\n.animated-top-dock-component.atd-modern,\n.animated-top-dock-component.atd-retro,\n.animated-top-dock-component.atd-glass {\n --atd-top: clamp(16px, 2.6cqw, 46px);\n --atd-bar-width: min(1460px, calc(100% - clamp(24px, 4.2cqw, 96px)));\n --atd-bar-height: clamp(48px, 3.4cqw, 68px);\n --atd-bar-pad: clamp(5px, 0.42cqw, 9px);\n --atd-bar-lead: clamp(14px, 1.15cqw, 25px);\n --atd-item-height: clamp(30px, 2.2cqw, 44px);\n --atd-item-pad: clamp(10px, 0.86cqw, 18px);\n --atd-label: clamp(12px, 0.78cqw, 17px);\n --atd-icon: clamp(13px, 0.92cqw, 19px);\n --atd-gap: clamp(5px, 0.44cqw, 9px);\n --atd-brand: clamp(13px, 0.94cqw, 20px);\n --atd-mark: clamp(22px, 1.55cqw, 33px);\n --atd-action-height: clamp(34px, 2.5cqw, 50px);\n display: block;\n overflow: hidden;\n}\n\n.atd-modern__bar,\n.atd-retro__bar {\n position: absolute;\n top: var(--atd-top);\n left: 50%;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: var(--atd-bar-width);\n height: var(--atd-bar-height);\n transform: translateX(-50%);\n}\n\n/* Command bar: the bar hugs its own content — brand, dock and actions sit in\n one flow with a gap between them rather than being pushed to the far edges of\n a full-bleed strip. That only holds because the dock's track is locked to its\n rest width (lockTrack in the controller): the spring then grows its items\n outside that track instead of widening the bar and shoving the brand and the\n actions around on every pointer move. */\n.atd-modern__bar {\n /* space-between is inherited rather than centred on purpose: at its natural\n width the three groups already fill the box, and in a shell too narrow for\n them the overflow then runs off the right instead of taking the brand off\n the left as well */\n width: max-content;\n max-width: var(--atd-bar-width);\n padding: var(--atd-bar-pad) var(--atd-bar-pad) var(--atd-bar-pad) var(--atd-bar-lead);\n}\n\n.atd-modern__dock {\n display: flex;\n flex: none;\n gap: clamp(2px, 0.2cqw, 5px);\n align-items: flex-start;\n /* the track is one item tall and its rest width wide, and the row is centred\n inside it, so a growing item overflows evenly on all four sides without\n moving a single neighbour in the bar */\n justify-content: center;\n height: var(--atd-item-height);\n}\n\n.atd-modern__item,\n.atd-retro__item,\n.atd-glass__item {\n display: inline-flex;\n flex: none;\n gap: var(--atd-gap);\n align-items: center;\n justify-content: center;\n height: var(--atd-item-height);\n padding: 0 var(--atd-item-pad);\n outline: none;\n white-space: nowrap;\n transform-origin: 50% 0;\n will-change: width, height, transform;\n cursor: pointer;\n}\n\n.atd-modern__brand,\n.atd-glass__brand,\n.atd-retro__brand {\n display: inline-flex;\n flex: none;\n gap: clamp(7px, 0.6cqw, 13px);\n align-items: center;\n text-decoration: none;\n}\n\n.atd-modern__mark,\n.atd-glass__mark {\n display: block;\n width: var(--atd-mark);\n height: var(--atd-mark);\n overflow: hidden;\n}\n\n.atd-modern__mark svg,\n.atd-glass__mark svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.atd-modern__word,\n.atd-glass__word {\n font-size: var(--atd-brand);\n font-weight: 600;\n letter-spacing: -0.018em;\n}\n\n.atd-modern__actions {\n display: inline-flex;\n flex: none;\n gap: clamp(4px, 0.34cqw, 8px);\n align-items: center;\n}\n\n.atd-modern__ghost {\n height: var(--atd-action-height);\n padding: 0 clamp(12px, 0.95cqw, 20px);\n border: 0;\n border-radius: 999px;\n background: transparent;\n font: 500 var(--atd-label) / 1 inherit;\n transition: color 0.16s, background 0.16s;\n cursor: pointer;\n}\n\n.atd-modern__cta,\n.atd-glass__cta {\n height: var(--atd-action-height);\n border-radius: 999px;\n font: 600 var(--atd-label) / 1 inherit;\n letter-spacing: -0.01em;\n white-space: nowrap;\n transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s;\n cursor: pointer;\n}\n\n/* ── Modern command bar ─────────────────────────────────────────────────── */\n\n.animated-top-dock-component.atd-modern {\n /* a ceiling rather than a width: the command bar sizes itself to its own\n cluster, and only falls back to an inset strip in a shell too narrow to\n hold it */\n --atd-bar-width: min(1240px, calc(100% - clamp(24px, 5cqw, 120px)));\n background: #07080c;\n}\n\n.atd-modern__aurora {\n position: absolute;\n inset: -24% -12% auto -12%;\n z-index: 1;\n height: 82%;\n pointer-events: none;\n background:\n radial-gradient(58% 62% at 22% 36%, rgba(78, 116, 255, 0.4), transparent 68%),\n radial-gradient(46% 56% at 78% 20%, rgba(186, 96, 255, 0.3), transparent 70%),\n radial-gradient(40% 48% at 52% 6%, rgba(56, 220, 214, 0.2), transparent 72%),\n radial-gradient(34% 40% at 50% 14%, rgba(255, 255, 255, 0.07), transparent 76%);\n filter: blur(8px);\n}\n\n.atd-modern__bar {\n gap: clamp(14px, 2.4cqw, 40px);\n border: 1px solid rgba(255, 255, 255, 0.045);\n border-radius: 999px;\n background: linear-gradient(180deg, rgba(25, 27, 36, 0.86), rgba(12, 13, 18, 0.82));\n box-shadow: 0 22px 52px -24px rgba(0, 0, 0, 0.92), inset 0 1px rgba(255, 255, 255, 0.045);\n backdrop-filter: blur(22px) saturate(150%);\n -webkit-backdrop-filter: blur(22px) saturate(150%);\n}\n\n.atd-modern__brand {\n color: #f2f3f7;\n}\n\n.atd-modern__mark {\n /* a full circle: the shared brand mark is a rounded square, so the disc is cut\n from it by the mark box's own clip rather than by a second svg */\n border-radius: 50%;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.42);\n}\n\n.atd-modern__item {\n border: 1px solid transparent;\n border-radius: 999px;\n color: #9b9fae;\n background: transparent;\n font: 500 var(--atd-label) / 1 inherit;\n letter-spacing: -0.005em;\n transition: color 0.16s, border-color 0.18s, background 0.18s, box-shadow 0.18s, backdrop-filter 0.18s;\n}\n\n/* :hover is carried alongside the proximity flag so the glass still lands when\n the spring is off — reduced motion, a coarse pointer, a narrow viewport */\n.atd-modern__item[data-dock-near=\"true\"],\n.atd-modern__item:hover,\n.atd-modern__item:focus-visible {\n color: #f4f5f9;\n border-color: rgba(255, 255, 255, 0.055);\n background: rgba(255, 255, 255, 0.055);\n box-shadow: 0 8px 20px -14px rgba(0, 0, 0, 0.9), inset 0 1px rgba(255, 255, 255, 0.05);\n backdrop-filter: blur(18px) saturate(170%);\n -webkit-backdrop-filter: blur(18px) saturate(170%);\n}\n\n.atd-modern__item[aria-pressed=\"true\"] {\n color: #0a0b10;\n border-color: transparent;\n background: linear-gradient(180deg, #ffffff, #dfe3ef);\n box-shadow: 0 10px 24px -12px rgba(120, 150, 255, 0.75), inset 0 -1px rgba(0, 0, 0, 0.14);\n}\n\n.atd-modern__icon,\n.atd-glass__icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: var(--atd-icon);\n height: var(--atd-icon);\n opacity: 0.72;\n transition: opacity 0.16s;\n}\n\n.atd-modern__icon svg,\n.atd-glass__icon svg {\n display: block;\n width: 100%;\n height: 100%;\n fill: none;\n stroke: currentColor;\n stroke-width: 1.25;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.atd-modern__item[data-dock-near=\"true\"] .atd-modern__icon,\n.atd-modern__item[aria-pressed=\"true\"] .atd-modern__icon,\n.atd-glass__item[data-dock-near=\"true\"] .atd-glass__icon,\n.atd-glass__item[aria-pressed=\"true\"] .atd-glass__icon {\n opacity: 1;\n}\n\n.atd-modern__ghost {\n color: #a7abba;\n}\n\n.atd-modern__ghost:hover {\n color: #f0f1f6;\n background: rgba(255, 255, 255, 0.06);\n}\n\n.atd-modern__cta {\n display: inline-flex;\n gap: var(--atd-gap);\n align-items: center;\n padding: 0 var(--atd-bar-pad) 0 clamp(14px, 1.1cqw, 24px);\n border: 0;\n color: #0a0b10;\n background: linear-gradient(180deg, #ffffff, #d8ddec);\n box-shadow: 0 10px 26px -14px rgba(150, 175, 255, 0.95), inset 0 -1px rgba(0, 0, 0, 0.16);\n}\n\n.atd-modern__cta svg {\n box-sizing: border-box;\n width: calc(var(--atd-action-height) - clamp(12px, 0.9cqw, 20px));\n height: calc(var(--atd-action-height) - clamp(12px, 0.9cqw, 20px));\n padding: clamp(4px, 0.3cqw, 7px);\n border-radius: 999px;\n background: rgba(10, 11, 16, 0.1);\n fill: none;\n stroke: currentColor;\n stroke-width: 1.4;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.atd-modern__cta:hover {\n box-shadow: 0 16px 32px -14px rgba(150, 175, 255, 1), inset 0 -1px rgba(0, 0, 0, 0.16);\n transform: translateY(-1px);\n}\n\n.atd-modern__stage {\n position: absolute;\n top: 58%;\n left: 50%;\n z-index: 4;\n width: min(88cqw, 1120px);\n text-align: center;\n pointer-events: none;\n transform: translate(-50%, -50%);\n}\n\n.atd-modern__eyebrow {\n margin: 0 0 clamp(8px, 0.9cqw, 18px);\n color: #7d84a0;\n font: 400 clamp(8px, 0.62cqw, 13px) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.22em;\n text-transform: uppercase;\n}\n\n.atd-modern__headline {\n /* background-clip: text paints only inside the element box, so at this size a\n 1.0-ish line-height would cut the ascenders and descenders out of the fill.\n The padding buys the ink its own room back. */\n margin: 0;\n padding: 0.08em 0 0.14em;\n color: transparent;\n background: linear-gradient(180deg, #ffffff 16%, rgba(255, 255, 255, 0.3));\n background-clip: text;\n -webkit-background-clip: text;\n font-size: clamp(30px, 7.6cqw, 150px);\n font-weight: 400;\n line-height: 1.04;\n letter-spacing: -0.03em;\n text-wrap: balance;\n}\n\n/* ── Retro pixel terminal ───────────────────────────────────────────────────\n The field canvas is a small drawing buffer stretched by CSS with\n nearest-neighbour sampling, so its dither pattern arrives as real square\n pixels. Every chrome edge here is square to sit on that same grid. */\n\n.animated-top-dock-component.atd-retro {\n --atd-label: clamp(9px, 0.68cqw, 15px);\n --atd-icon: clamp(10px, 0.78cqw, 17px);\n --atd-brand: clamp(11px, 0.8cqw, 18px);\n background: #0b0819;\n}\n\n.atd-retro__field {\n position: absolute;\n inset: 0;\n z-index: 0;\n display: block;\n width: 100%;\n height: 100%;\n image-rendering: pixelated;\n}\n\n.atd-retro__vignette {\n position: absolute;\n inset: 0;\n z-index: 1;\n pointer-events: none;\n background:\n radial-gradient(124% 90% at 50% 34%, transparent 28%, rgba(6, 4, 16, 0.62) 100%),\n linear-gradient(180deg, rgba(6, 4, 16, 0.6) 0%, transparent 24%, transparent 62%, rgba(6, 4, 16, 0.58) 100%);\n}\n\n/* the raster runs at screen resolution, not at the field's pixel scale, so the\n line structure stays a raster instead of banding the chunky artwork */\n.atd-retro::before {\n position: absolute;\n inset: 0;\n z-index: 11;\n pointer-events: none;\n content: \"\";\n opacity: calc(var(--atd-retro-scan, 0.32) * 1.5);\n background: repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.42) 0 1px, transparent 1px 3px);\n}\n\n/* Horizontal fit: the bar is one continuous strip and the menu cells own all of\n it. There are no gaps and no rounded ends — every cell is separated by a rule\n and runs the full height, and the controller renormalises the widths so\n opening one cell takes its room from the neighbours. */\n.atd-retro__bar {\n gap: 0;\n padding: 0;\n overflow: hidden;\n border: 2px solid #f4e6c8;\n background: #1a1030;\n box-shadow: 6px 6px 0 rgba(6, 4, 16, 0.62);\n}\n\n.atd-retro__brand {\n height: 100%;\n padding: 0 clamp(12px, 1.1cqw, 26px);\n border-right: 2px solid #4b2f7e;\n color: #f9c74f;\n background: #150c28;\n font: 400 var(--atd-brand) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.12em;\n}\n\n.atd-retro__dock {\n display: flex;\n flex: 1 1 0;\n gap: 0;\n align-items: stretch;\n min-width: 0;\n height: 100%;\n}\n\n.atd-retro__badge {\n display: inline-flex;\n width: var(--atd-icon);\n height: var(--atd-icon);\n}\n\n.atd-retro__badge svg,\n.atd-retro__icon svg {\n display: block;\n width: 100%;\n height: 100%;\n fill: currentColor;\n shape-rendering: crispEdges;\n}\n\n.atd-retro__item {\n height: 100%;\n min-width: 0;\n padding: 0 clamp(6px, 0.5cqw, 12px);\n overflow: hidden;\n border: 0;\n border-right: 2px solid #4b2f7e;\n color: #b9a7e8;\n background: #241548;\n font: 400 var(--atd-label) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.14em;\n transition: color 0.1s steps(2), background 0.1s steps(2);\n}\n\n.atd-retro__item[data-dock-near=\"true\"],\n.atd-retro__item:focus-visible {\n color: #fff4d6;\n background: #3a2170;\n}\n\n.atd-retro__item[aria-pressed=\"true\"] {\n color: #1a1030;\n background: #f9c74f;\n}\n\n.atd-retro__icon {\n display: inline-flex;\n flex: none;\n width: var(--atd-icon);\n height: var(--atd-icon);\n}\n\n.atd-retro__cta {\n display: inline-flex;\n flex: none;\n gap: var(--atd-gap);\n align-items: center;\n height: 100%;\n padding: 0 clamp(14px, 1.2cqw, 28px);\n border: 0;\n border-left: 2px solid #f4e6c8;\n color: #1a1030;\n background: #f47b5c;\n font: 400 var(--atd-label) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.16em;\n transition: background 0.1s steps(2), color 0.1s steps(2);\n cursor: pointer;\n}\n\n.atd-retro__cta:hover {\n color: #1a1030;\n background: #f9c74f;\n}\n\n.atd-retro__readout {\n position: absolute;\n bottom: clamp(30px, 4.2cqw, 74px);\n left: 50%;\n z-index: 8;\n display: flex;\n gap: clamp(12px, 2.4cqw, 42px);\n margin: 0;\n color: #a08fd4;\n font: 400 clamp(9px, 0.62cqw, 13px) / 1 \"ThreeUI Fragment Mono\", ui-monospace, monospace;\n letter-spacing: 0.2em;\n transform: translateX(-50%);\n}\n\n/* ── Liquid glass ───────────────────────────────────────────────────────────\n Vertical fit: the dock leaves the top edge for a rail down the left side. The\n proximity field runs on the y axis, so an item grows taller and leans out to\n the right instead of widening and dropping. The rail is a real\n backdrop-filter panel over the refracting Three.js bead field, so its blur\n samples the same pixels the glass shader refracts. */\n\n.animated-top-dock-component.atd-glass {\n --atd-rail-width: clamp(158px, 12.5cqw, 250px);\n background: #08090d;\n}\n\n.atd-glass__field {\n position: absolute;\n inset: 0;\n z-index: 0;\n display: block;\n width: 100%;\n height: 100%;\n}\n\n/* a scrim behind the rail: the bead field is bright enough in places to swallow\n the wordmark, and the panel's own blur cannot darken what it samples */\n.atd-glass::before {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 5;\n width: 42%;\n pointer-events: none;\n content: \"\";\n background: linear-gradient(90deg, rgba(6, 7, 11, 0.66), rgba(6, 7, 11, 0));\n}\n\n.atd-glass__rail {\n position: absolute;\n top: 50%;\n left: clamp(20px, 3.4cqw, 68px);\n z-index: 10;\n display: flex;\n flex-direction: column;\n gap: clamp(6px, 0.5cqw, 11px);\n align-items: stretch;\n width: var(--atd-rail-width);\n padding: var(--atd-bar-pad);\n border-radius: clamp(20px, 1.7cqw, 34px);\n background: linear-gradient(160deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05));\n box-shadow:\n 0 30px 64px -26px rgba(0, 0, 0, 0.92),\n inset 0 1px 0 rgba(255, 255, 255, 0.44),\n inset 0 -1px 0 rgba(255, 255, 255, 0.12),\n inset 0 0 0 1px rgba(255, 255, 255, 0.14);\n backdrop-filter: blur(26px) saturate(190%);\n -webkit-backdrop-filter: blur(26px) saturate(190%);\n transform: translateY(-50%);\n}\n\n.atd-glass__rail::before {\n position: absolute;\n inset: 0;\n pointer-events: none;\n content: \"\";\n border-radius: inherit;\n background: linear-gradient(168deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0) 76%, rgba(255, 255, 255, 0.14) 100%);\n}\n\n.atd-glass__brand {\n position: relative;\n padding: clamp(6px, 0.5cqw, 12px) clamp(8px, 0.7cqw, 15px);\n color: #fbfbfe;\n}\n\n.atd-glass__mark {\n border-radius: clamp(7px, 0.5cqw, 11px);\n box-shadow: 0 3px 10px rgba(0, 0, 0, 0.42), inset 0 0 0 1px rgba(255, 255, 255, 0.3);\n}\n\n.atd-glass__hairline {\n height: 1px;\n margin: clamp(2px, 0.2cqw, 5px) clamp(6px, 0.5cqw, 12px);\n background: linear-gradient(90deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.04));\n}\n\n.atd-glass__dock {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: clamp(2px, 0.2cqw, 5px);\n align-items: stretch;\n}\n\n.atd-glass__item {\n justify-content: flex-start;\n width: 100%;\n border: 1px solid transparent;\n border-radius: clamp(12px, 1cqw, 22px);\n color: rgba(255, 255, 255, 0.66);\n background: transparent;\n font: 500 var(--atd-label) / 1 inherit;\n letter-spacing: -0.005em;\n /* the rail grows downward from each item's own top edge */\n transform-origin: 0 50%;\n transition: color 0.18s, border-color 0.2s, background 0.2s, box-shadow 0.2s;\n}\n\n.atd-glass__item[data-dock-near=\"true\"],\n.atd-glass__item:focus-visible {\n color: #ffffff;\n border-color: rgba(255, 255, 255, 0.24);\n background: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.07));\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.46), 0 8px 18px -10px rgba(0, 0, 0, 0.8);\n}\n\n.atd-glass__item[aria-pressed=\"true\"] {\n color: #0d0f16;\n border-color: rgba(255, 255, 255, 0.6);\n background: linear-gradient(180deg, #ffffff, #e6e9f4);\n box-shadow: 0 10px 24px -12px rgba(255, 255, 255, 0.5), inset 0 -1px rgba(0, 0, 0, 0.14);\n}\n\n.atd-glass__cta {\n display: inline-flex;\n gap: var(--atd-gap);\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 0 clamp(12px, 1cqw, 22px);\n border: 1px solid rgba(255, 255, 255, 0.55);\n color: #0d0f16;\n background: linear-gradient(180deg, #ffffff, #dfe3f0);\n box-shadow: 0 12px 30px -14px rgba(255, 255, 255, 0.6), inset 0 -1px rgba(0, 0, 0, 0.16);\n}\n\n.atd-glass__cta svg {\n width: var(--atd-icon);\n height: var(--atd-icon);\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.atd-glass__cta:hover {\n box-shadow: 0 16px 34px -14px rgba(255, 255, 255, 0.75), inset 0 -1px rgba(0, 0, 0, 0.16);\n transform: translateY(-1px);\n}\n\n.atd-modern .animated-top-dock-component__caption,\n.atd-glass .animated-top-dock-component__caption {\n color: rgba(255, 255, 255, 0.34);\n}\n\n.atd-retro .animated-top-dock-component__caption {\n z-index: 12;\n color: #7b6ab0;\n}\n\n@container (max-width: 900px) {\n .atd-modern__ghost {\n display: none;\n }\n\n .atd-modern__item span:last-child,\n .atd-retro__item span:last-child {\n display: none;\n }\n\n /* the bar is sized by its content now, so a shell this narrow has to give the\n content back: the call to action drops to its glyph rather than pushing the\n cluster past the bar's own max width */\n .atd-modern__cta span {\n display: none;\n }\n\n .atd-modern__cta {\n padding: 0 var(--atd-bar-pad);\n }\n\n /* the rail collapses to its icons rather than keeping a 158 px column in a\n card that is only a few hundred pixels wide */\n .animated-top-dock-component.atd-glass {\n --atd-rail-width: auto;\n }\n\n .atd-glass__item {\n justify-content: center;\n }\n\n .atd-glass__item span:last-child,\n .atd-glass__word,\n .atd-glass__cta span {\n display: none;\n }\n\n .atd-glass__cta {\n justify-content: center;\n }\n}\n\n@media (max-width: 600px) {\n .atd-modern__word {\n display: none;\n }\n\n .atd-modern__item {\n height: 28px !important;\n transform: none !important;\n }\n\n .atd-glass__item {\n height: auto !important;\n transform: none !important;\n }\n\n /* the strip keeps its full-height cells on a phone; only the spring stops */\n .atd-retro__item {\n height: 100% !important;\n transform: none !important;\n }\n\n .atd-modern__stage {\n top: 62%;\n }\n\n .atd-retro__readout {\n display: none;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .atd-modern__item,\n .atd-retro__item,\n .atd-glass__item {\n transform: none !important;\n }\n}\n" }, { "path": "src/shaders/japanese-tower/Towers.html", @@ -637,28 +637,37 @@ "path": "src/shaders/crt/CrtBackground.tsx", "language": "tsx", "role": "component", - "bytes": 1772, - "lines": 10, - "sha256": "c1aa200784432a7abf8023179b8509eb18e1bd38289656e1ca058d1cc161719b", - "code": "import { useEffect, useRef } from \"react\";\nimport { createCrtRenderer, CRT_DEFAULTS, type CrtOptions } from \"./crtRenderer\";\n\nexport type CrtBackgroundProps = Partial & { className?: string };\nexport function CrtBackground({ className = \"\", ...props }: CrtBackgroundProps) {\n const hostRef = useRef(null), canvasRef = useRef(null), optionsRef = useRef({ ...CRT_DEFAULTS, ...props }); optionsRef.current = { ...CRT_DEFAULTS, ...props };\n useEffect(() => { const host = hostRef.current, canvas = canvasRef.current; if (!host || !canvas) return undefined; const renderer = createCrtRenderer(host, canvas, () => optionsRef.current); let frame = 0, visible = true; const resize = () => { renderer.resize(); renderer.render(performance.now()); }, tick = (now: number) => { renderer.render(now); frame = visible && !document.hidden ? requestAnimationFrame(tick) : 0; }; const resizeObserver = new ResizeObserver(resize), intersection = new IntersectionObserver(([entry]) => { visible = entry?.isIntersecting ?? true; if (visible && !frame) frame = requestAnimationFrame(tick); if (!visible && frame) cancelAnimationFrame(frame), frame = 0; }); resizeObserver.observe(host); intersection.observe(host); resize(); frame = requestAnimationFrame(tick); return () => { if (frame) cancelAnimationFrame(frame); resizeObserver.disconnect(); intersection.disconnect(); renderer.dispose(); }; }, []);\n const options = optionsRef.current; return
;\n}\n" + "bytes": 1947, + "lines": 13, + "sha256": "20932f2655319c5fc6c6b3c29c890149beec7e4850edc414f909ab24a0c95031", + "code": "import { useEffect, useRef } from \"react\";\nimport { createCrtRenderer, crtStyle, CRT_DEFAULTS, CRT_VARIANTS, type CrtOptions } from \"./crtRenderer\";\nimport type { CrtVariant } from \"./crtScreens\";\n\nexport { CRT_VARIANTS };\nexport type { CrtVariant };\nexport type CrtBackgroundProps = Partial & { className?: string };\nexport function CrtBackground({ className = \"\", ...props }: CrtBackgroundProps) {\n const hostRef = useRef(null), canvasRef = useRef(null), optionsRef = useRef({ ...CRT_DEFAULTS, ...props }); optionsRef.current = { ...CRT_DEFAULTS, ...props };\n useEffect(() => { const host = hostRef.current, canvas = canvasRef.current; if (!host || !canvas) return undefined; const renderer = createCrtRenderer(host, canvas, () => optionsRef.current); let frame = 0, visible = true; const resize = () => { renderer.resize(); renderer.render(performance.now()); }, tick = (now: number) => { renderer.render(now); frame = visible && !document.hidden ? requestAnimationFrame(tick) : 0; }; const resizeObserver = new ResizeObserver(resize), intersection = new IntersectionObserver(([entry]) => { visible = entry?.isIntersecting ?? true; if (visible && !frame) frame = requestAnimationFrame(tick); if (!visible && frame) cancelAnimationFrame(frame), frame = 0; }); resizeObserver.observe(host); intersection.observe(host); resize(); frame = requestAnimationFrame(tick); return () => { if (frame) cancelAnimationFrame(frame); resizeObserver.disconnect(); intersection.disconnect(); renderer.dispose(); }; }, []);\n const options = optionsRef.current; return
;\n}\n" }, { "path": "src/shaders/crt/crtRenderer.ts", "language": "typescript", "role": "renderer-source", - "bytes": 8896, - "lines": 35, - "sha256": "beb19dd637bd6d618f9d4f3c494261d8c8ea5bf8ca6e7ce005818805831dd99a", - "code": "import { CRT_FRAGMENT_SHADER, CRT_VERTEX_SHADER } from \"./crtShaders\";\n\nexport type CrtOptions = { speed: number; typeSpeed: number; motion: number; brightness: number; opacity: number; hue: number; saturation: number };\nexport const CRT_DEFAULTS: CrtOptions = { speed: 1, typeSpeed: 1, motion: 1, brightness: 1, opacity: 1, hue: 0, saturation: 1 };\ntype Segment = { t: string; c: \"p\" | \"d\" | \"a\" | \"h\" };\nconst segment = (text: string, color: Segment[\"c\"] = \"p\"): Segment => ({ t: text, c: color }); const dots = (count: number) => \"·\".repeat(count);\nconst LOG: Segment[][] = [\n [segment(\"ZION MAINFRAME v9.1.1\"), segment(\" (c) 2199 Nebuchadnezzar\", \"d\")], [segment(\"CONSTRUCT Broadcast Rev M S/N NX-0101-0011\", \"d\")], [],\n [segment(\"Hacking Matrix grid nodes \"), segment(`${dots(14)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Neural Jack 0x000-0x0FF \"), segment(`${dots(11)} `, \"d\"), segment(\"ONLINE \"), segment(\"OK\", \"a\")], [segment(\"Pinging agent signatures \"), segment(`${dots(6)} `, \"d\"), segment(\"3 found\")],\n [segment(\"nav0 OPERATOR UPLINK SECURE \", \"d\"), segment(`${dots(6)} `, \"d\"), segment(\"READY\", \"a\")], [segment(\"vis0 CODE RAIN DECRYPT 256bit \", \"d\"), segment(\"READY\", \"a\")], [segment(\"net0 HARDLINE CONNECTION MAX \", \"d\"), segment(`${dots(4)} `, \"d\"), segment(\"LINK\", \"a\")], [segment(\"red0 RED PILL EXTRACTION \", \"d\"), segment(`${dots(4)} `, \"d\"), segment(\"READY\", \"a\")],\n [segment(\"Mounting /dev/mind -> ROOT: \"), segment(`${dots(6)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Loading weapon training program \"), segment(`${dots(4)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Starting [ jmp spd str wpn ] \"), segment(`${dots(4)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Locating the Oracle sector \"), segment(`${dots(6)} `, \"d\"), segment(\"99.9%\")], [],\n [segment(\"SYSTEM ANOMALY \"), segment(\"detected.\", \"h\")], [segment(\"subject Thomas A. Anderson status asleep \", \"d\"), segment(\"z\", \"d\"), segment(\"Z\", \"d\")], [], [segment(\"wake up: \")],\n];\nconst COLORS = { p: { fill: \"#8df0b4\", glow: \"rgba(28,236,132,0.95)\" }, d: { fill: \"#4f9a76\", glow: \"rgba(28,236,132,0.45)\" }, a: { fill: \"#ffba5e\", glow: \"rgba(255,150,52,0.95)\" }, h: { fill: \"#eafff3\", glow: \"rgba(120,255,190,0.95)\" } };\nconst lineLength = (line: Segment[]) => line.reduce((total, item) => total + item.t.length, 0); const TOTAL = LOG.reduce((total, line) => total + lineLength(line), 0); const MAX_CHARS = Math.max(...LOG.map(lineLength));\nfunction compile(gl: WebGLRenderingContext, type: number, source: string) { const shader = gl.createShader(type); if (!shader) throw new Error(\"Unable to create CRT shader\"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) ?? \"CRT shader compilation failed\"); return shader; }\n\nexport function createCrtRenderer(host: HTMLElement, canvas: HTMLCanvasElement, getOptions: () => CrtOptions) {\n const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: false, depth: false, premultipliedAlpha: false }); if (!gl) throw new Error(\"CRT requires WebGL\"); const textCanvas = document.createElement(\"canvas\"), textContext = textCanvas.getContext(\"2d\"); if (!textContext) throw new Error(\"CRT text canvas unavailable\");\n const vertex = compile(gl, gl.VERTEX_SHADER, CRT_VERTEX_SHADER), fragment = compile(gl, gl.FRAGMENT_SHADER, CRT_FRAGMENT_SHADER), program = gl.createProgram(); if (!program) throw new Error(\"Unable to create CRT program\"); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? \"CRT link failed\"); gl.useProgram(program);\n const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); const position = gl.getAttribLocation(program, \"aPos\"); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n const uTexture = gl.getUniformLocation(program, \"uTex\"), uResolution = gl.getUniformLocation(program, \"uRes\"), uTime = gl.getUniformLocation(program, \"uTime\"), uMotion = gl.getUniformLocation(program, \"uMotion\"), texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.uniform1i(uTexture, 0);\n let width = 1, height = 1, fontSize = 14, lineHeight = 20, startY = 0, charWidth = 8, caretX = 0, caretY = 0, typed = 0, done = false, textDirty = true, lastTextAt = 0, lastReveal = -1, lastBlink = -1; const startedAt = performance.now();\n const layout = () => { startY = height * 0.135; lineHeight = height * 0.74 / LOG.length; fontSize = Math.max(5, Math.min(lineHeight * 0.8, width * 0.88 / (Math.max(MAX_CHARS, 1) * 0.62))); textContext.font = `600 ${fontSize.toFixed(2)}px ui-monospace, \"SF Mono\", \"JetBrains Mono\", Menlo, Consolas, monospace`; charWidth = textContext.measureText(\"M\").width || fontSize * 0.6; };\n const setStyle = (key: Segment[\"c\"]) => { const color = COLORS[key]; textContext.fillStyle = color.fill; textContext.shadowColor = color.glow; textContext.shadowBlur = fontSize * 0.55; };\n const drawScreen = (reveal: number) => { textContext.setTransform(1, 0, 0, 1, 0, 0); textContext.fillStyle = \"#03100a\"; textContext.fillRect(0, 0, width, height); textContext.textBaseline = \"top\"; textContext.font = `600 ${fontSize.toFixed(2)}px ui-monospace, \"SF Mono\", \"JetBrains Mono\", Menlo, Consolas, monospace`; let remaining = reveal, y = startY; caretX = Math.floor((width - MAX_CHARS * charWidth) / 2); caretY = startY;\n for (const line of LOG) { const length = lineLength(line), visible = reveal === Infinity ? Infinity : Math.min(remaining, length); let x = Math.floor((width - MAX_CHARS * charWidth) / 2), drawn = 0; for (const item of line) { let text = item.t; if (visible !== Infinity) { const left = visible - drawn; if (left <= 0) break; if (left < text.length) text = text.slice(0, left); } if (text.length) { setStyle(item.c); textContext.fillText(text, x, y); x += charWidth * text.length; } drawn += item.t.length; if (visible !== Infinity && drawn >= visible) break; } caretX = x; caretY = y; if (visible !== Infinity) remaining -= visible; y += lineHeight; if (visible !== Infinity && remaining <= 0) break; }\n };\n const drawCursor = () => { textContext.shadowColor = COLORS.p.glow; textContext.shadowBlur = fontSize * 0.6; textContext.fillStyle = \"#bdf8d2\"; textContext.fillRect(caretX, caretY + fontSize * 0.06, Math.max(charWidth * 0.92, 4), fontSize * 0.96); };\n const uploadTexture = () => { gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); textDirty = false; };\n const resize = () => { const bounds = host.getBoundingClientRect(), viewportWidth = Math.max(1, bounds.width), viewportHeight = Math.max(1, bounds.height), scale = viewportWidth < 700 ? 0.82 : 0.55, bufferWidth = Math.min(Math.round(viewportWidth * scale), 920), bufferHeight = Math.round(bufferWidth * viewportHeight / viewportWidth); if (canvas.width !== bufferWidth || canvas.height !== bufferHeight || width !== bufferWidth) { canvas.width = bufferWidth; canvas.height = bufferHeight; textCanvas.width = bufferWidth; textCanvas.height = bufferHeight; width = bufferWidth; height = bufferHeight; layout(); lastReveal = -1; lastBlink = -1; } gl.viewport(0, 0, bufferWidth, bufferHeight); gl.uniform2f(uResolution, bufferWidth, bufferHeight); };\n const maybeRedrawText = (now: number) => { const reveal = done ? Infinity : Math.floor(typed), blink = Math.floor((now - startedAt) / 420) % 2 === 0 ? 1 : 0, due = !done ? now - lastTextAt > 42 : blink !== lastBlink; if (reveal === lastReveal && blink === lastBlink && !due) return; if (!done && now - lastTextAt <= 42 && reveal === lastReveal && blink === lastBlink) return; drawScreen(reveal); if (blink) drawCursor(); lastTextAt = now; lastReveal = reveal; lastBlink = blink; textDirty = true; };\n return { resize, render(now: number) { const options = getOptions(); if (!done) { typed += 4.4 * options.typeSpeed; if (typed >= TOTAL) { typed = TOTAL; done = true; } } maybeRedrawText(now); if (textDirty) uploadTexture(); gl.useProgram(program); gl.uniform1f(uTime, (now - startedAt) * 0.001 * options.speed); gl.uniform1f(uMotion, options.motion); gl.drawArrays(gl.TRIANGLES, 0, 3); }, dispose() { gl.deleteBuffer(buffer); gl.deleteTexture(texture); gl.deleteProgram(program); gl.deleteShader(vertex); gl.deleteShader(fragment); } };\n}\n" + "bytes": 12561, + "lines": 63, + "sha256": "a3eb536e9c50eeb31832e7d6d25021c1535137e8ead5eb1b864e5a27c340af03", + "code": "import { CRT_SCREENS, CRT_STYLES, type CrtStyle, type CrtVariant } from \"./crtScreens\";\nimport { CRT_FRAGMENT_SHADER, CRT_VERTEX_SHADER } from \"./crtShaders\";\n\nexport const CRT_VARIANTS = [\"terminal\", \"cinematic\", \"blue-screen\", \"nintendo\"] as const;\nexport type CrtOptions = { variant: CrtVariant; speed: number; typeSpeed: number; motion: number; brightness: number; opacity: number; hue: number; saturation: number };\nexport const CRT_DEFAULTS: CrtOptions = { variant: \"terminal\", speed: 1, typeSpeed: 1, motion: 1, brightness: 1, opacity: 1, hue: 0, saturation: 1 };\nexport const crtStyle = (variant: CrtVariant): CrtStyle => CRT_STYLES[variant] ?? CRT_STYLES.terminal;\ntype Segment = { t: string; c: \"p\" | \"d\" | \"a\" | \"h\" };\nconst segment = (text: string, color: Segment[\"c\"] = \"p\"): Segment => ({ t: text, c: color }); const dots = (count: number) => \"·\".repeat(count);\nconst LOG: Segment[][] = [\n [segment(\"ZION MAINFRAME v9.1.1\"), segment(\" (c) 2199 Nebuchadnezzar\", \"d\")], [segment(\"CONSTRUCT Broadcast Rev M S/N NX-0101-0011\", \"d\")], [],\n [segment(\"Hacking Matrix grid nodes \"), segment(`${dots(14)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Neural Jack 0x000-0x0FF \"), segment(`${dots(11)} `, \"d\"), segment(\"ONLINE \"), segment(\"OK\", \"a\")], [segment(\"Pinging agent signatures \"), segment(`${dots(6)} `, \"d\"), segment(\"3 found\")],\n [segment(\"nav0 OPERATOR UPLINK SECURE \", \"d\"), segment(`${dots(6)} `, \"d\"), segment(\"READY\", \"a\")], [segment(\"vis0 CODE RAIN DECRYPT 256bit \", \"d\"), segment(\"READY\", \"a\")], [segment(\"net0 HARDLINE CONNECTION MAX \", \"d\"), segment(`${dots(4)} `, \"d\"), segment(\"LINK\", \"a\")], [segment(\"red0 RED PILL EXTRACTION \", \"d\"), segment(`${dots(4)} `, \"d\"), segment(\"READY\", \"a\")],\n [segment(\"Mounting /dev/mind -> ROOT: \"), segment(`${dots(6)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Loading weapon training program \"), segment(`${dots(4)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Starting [ jmp spd str wpn ] \"), segment(`${dots(4)} `, \"d\"), segment(\"OK\", \"a\")], [segment(\"Locating the Oracle sector \"), segment(`${dots(6)} `, \"d\"), segment(\"99.9%\")], [],\n [segment(\"SYSTEM ANOMALY \"), segment(\"detected.\", \"h\")], [segment(\"subject Thomas A. Anderson status asleep \", \"d\"), segment(\"z\", \"d\"), segment(\"Z\", \"d\")], [], [segment(\"wake up: \")],\n];\nconst COLORS = { p: { fill: \"#8df0b4\", glow: \"rgba(28,236,132,0.95)\" }, d: { fill: \"#4f9a76\", glow: \"rgba(28,236,132,0.45)\" }, a: { fill: \"#ffba5e\", glow: \"rgba(255,150,52,0.95)\" }, h: { fill: \"#eafff3\", glow: \"rgba(120,255,190,0.95)\" } };\nconst lineLength = (line: Segment[]) => line.reduce((total, item) => total + item.t.length, 0); const TOTAL = LOG.reduce((total, line) => total + lineLength(line), 0); const MAX_CHARS = Math.max(...LOG.map(lineLength));\n/* backing-store ceiling: the composite is one triangle, so the cost that matters is\n the 2D screen redraw and its upload, not the fragment pass */\nconst MAX_BUFFER_WIDTH = 1920, MIN_BUFFER_WIDTH = 640, MAX_BUFFER_PIXELS = 2_400_000;\nfunction compile(gl: WebGLRenderingContext, type: number, source: string) { const shader = gl.createShader(type); if (!shader) throw new Error(\"Unable to create CRT shader\"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) ?? \"CRT shader compilation failed\"); return shader; }\n\nexport function createCrtRenderer(host: HTMLElement, canvas: HTMLCanvasElement, getOptions: () => CrtOptions) {\n const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: false, depth: false, premultipliedAlpha: false }); if (!gl) throw new Error(\"CRT requires WebGL\"); const textCanvas = document.createElement(\"canvas\"), textContext = textCanvas.getContext(\"2d\"); if (!textContext) throw new Error(\"CRT text canvas unavailable\");\n const vertex = compile(gl, gl.VERTEX_SHADER, CRT_VERTEX_SHADER), fragment = compile(gl, gl.FRAGMENT_SHADER, CRT_FRAGMENT_SHADER), program = gl.createProgram(); if (!program) throw new Error(\"Unable to create CRT program\"); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program) ?? \"CRT link failed\"); gl.useProgram(program);\n const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); const position = gl.getAttribLocation(program, \"aPos\"); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n const uniform = (name: string) => gl.getUniformLocation(program, name);\n const uTexture = uniform(\"uTex\"), uResolution = uniform(\"uRes\"), uTime = uniform(\"uTime\"), uMotion = uniform(\"uMotion\"), uCurve = uniform(\"uCurve\"), uScan = uniform(\"uScan\"), uScanDepth = uniform(\"uScanDepth\"), uTriad = uniform(\"uTriad\"), uGrille = uniform(\"uGrille\"), uChroma = uniform(\"uChroma\"), uBar = uniform(\"uBar\"), uFlicker = uniform(\"uFlicker\"), uGrain = uniform(\"uGrain\"), uNoise = uniform(\"uNoise\"), uVignette = uniform(\"uVignette\"), uMono = uniform(\"uMono\"), uGain = uniform(\"uGain\"), uHalo = uniform(\"uHalo\"), uSheen = uniform(\"uSheen\"), uRoom = uniform(\"uRoom\");\n const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.uniform1i(uTexture, 0);\n let width = 1, height = 1, cssWidth = 1, cssHeight = 1, fontSize = 14, lineHeight = 20, startY = 0, charWidth = 8, caretX = 0, caretY = 0, typed = 0, done = false, textDirty = true, lastTextAt = 0, lastReveal = -1, lastBlink = -1, variant: CrtVariant = \"terminal\", style = crtStyle(variant); const startedAt = performance.now();\n const applyStyle = () => { gl.useProgram(program); gl.uniform2f(uCurve, style.curve[0], style.curve[1]); gl.uniform1f(uScanDepth, style.scanDepth); gl.uniform1f(uGrille, style.grille); gl.uniform1f(uChroma, style.chroma); gl.uniform1f(uBar, style.bar); gl.uniform1f(uFlicker, style.flicker); gl.uniform1f(uGrain, style.grain); gl.uniform1f(uNoise, style.noise); gl.uniform1f(uVignette, style.vignette); gl.uniform1f(uMono, style.mono); gl.uniform1f(uGain, style.gain); gl.uniform1f(uHalo, style.halo); gl.uniform3f(uSheen, style.sheen[0], style.sheen[1], style.sheen[2]); gl.uniform3f(uRoom, style.room[0], style.room[1], style.room[2]); const filter = style.filtering === \"nearest\" ? gl.NEAREST : gl.LINEAR; gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); };\n const layout = () => { startY = height * 0.135; lineHeight = height * 0.74 / LOG.length; fontSize = Math.max(5, Math.min(lineHeight * 0.8, width * 0.88 / (Math.max(MAX_CHARS, 1) * 0.62))); textContext.font = `600 ${fontSize.toFixed(2)}px ui-monospace, \"SF Mono\", \"JetBrains Mono\", Menlo, Consolas, monospace`; charWidth = textContext.measureText(\"M\").width || fontSize * 0.6; };\n const setStyle = (key: Segment[\"c\"], glow: boolean) => { const color = COLORS[key]; textContext.fillStyle = color.fill; textContext.shadowColor = glow ? color.glow : \"transparent\"; textContext.shadowBlur = glow ? fontSize * 0.38 : 0; };\n /* two passes per glyph: a soft phosphor halo, then the same glyph re-filled with\n the shadow off so the stroke core stays crisp at any backing resolution */\n const drawScreen = (reveal: number) => { textContext.setTransform(1, 0, 0, 1, 0, 0); textContext.fillStyle = \"#03100a\"; textContext.fillRect(0, 0, width, height); textContext.textAlign = \"left\"; textContext.textBaseline = \"top\"; textContext.font = `600 ${fontSize.toFixed(2)}px ui-monospace, \"SF Mono\", \"JetBrains Mono\", Menlo, Consolas, monospace`; let remaining = reveal, y = startY; caretX = Math.floor((width - MAX_CHARS * charWidth) / 2); caretY = startY;\n for (const line of LOG) { const length = lineLength(line), visible = reveal === Infinity ? Infinity : Math.min(remaining, length); let x = Math.floor((width - MAX_CHARS * charWidth) / 2), drawn = 0; for (const item of line) { let text = item.t; if (visible !== Infinity) { const left = visible - drawn; if (left <= 0) break; if (left < text.length) text = text.slice(0, left); } if (text.length) { setStyle(item.c, true); textContext.fillText(text, x, y); setStyle(item.c, false); textContext.fillText(text, x, y); x += charWidth * text.length; } drawn += item.t.length; if (visible !== Infinity && drawn >= visible) break; } caretX = x; caretY = y; if (visible !== Infinity) remaining -= visible; y += lineHeight; if (visible !== Infinity && remaining <= 0) break; }\n };\n const drawCursor = () => { textContext.shadowColor = COLORS.p.glow; textContext.shadowBlur = fontSize * 0.42; textContext.fillStyle = \"#bdf8d2\"; textContext.fillRect(caretX, caretY + fontSize * 0.06, Math.max(charWidth * 0.92, 4), fontSize * 0.96); textContext.shadowBlur = 0; textContext.fillRect(caretX, caretY + fontSize * 0.06, Math.max(charWidth * 0.92, 4), fontSize * 0.96); };\n const uploadTexture = () => { gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); textDirty = false; };\n const resize = () => { const bounds = host.getBoundingClientRect(); cssWidth = Math.max(1, bounds.width); cssHeight = Math.max(1, bounds.height); const density = Math.min(typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1, 2); let nextWidth = Math.max(MIN_BUFFER_WIDTH, Math.round(Math.min(cssWidth * density, MAX_BUFFER_WIDTH))), nextHeight = Math.max(1, Math.round(nextWidth * cssHeight / cssWidth)); if (nextWidth * nextHeight > MAX_BUFFER_PIXELS) { const fit = Math.sqrt(MAX_BUFFER_PIXELS / (nextWidth * nextHeight)); nextWidth = Math.round(nextWidth * fit); nextHeight = Math.round(nextHeight * fit); }\n const surface = style.surface, screenWidth = surface.mode === \"fixed\" ? surface.width : surface.mode === \"cap\" ? Math.min(nextWidth, surface.width) : nextWidth, screenHeight = surface.mode === \"fixed\" ? surface.height : Math.max(1, Math.round(screenWidth * nextHeight / nextWidth));\n if (canvas.width !== nextWidth || canvas.height !== nextHeight) { canvas.width = nextWidth; canvas.height = nextHeight; }\n if (textCanvas.width !== screenWidth || textCanvas.height !== screenHeight) { textCanvas.width = screenWidth; textCanvas.height = screenHeight; width = screenWidth; height = screenHeight; layout(); lastReveal = -1; lastBlink = -1; lastTextAt = 0; textDirty = true; }\n gl.useProgram(program); gl.viewport(0, 0, nextWidth, nextHeight); gl.uniform2f(uResolution, nextWidth, nextHeight); gl.uniform1f(uScan, Math.max(120, Math.min(cssHeight * style.scanDensity, 900))); gl.uniform1f(uTriad, Math.max(2, style.triadCss * nextWidth / cssWidth)); };\n const maybeRedrawText = (now: number) => { const reveal = done ? Infinity : Math.floor(typed), blink = Math.floor((now - startedAt) / 420) % 2 === 0 ? 1 : 0, due = !done ? now - lastTextAt > 42 : blink !== lastBlink; if (reveal === lastReveal && blink === lastBlink && !due) return; if (!done && now - lastTextAt <= 42 && reveal === lastReveal && blink === lastBlink) return; drawScreen(reveal); if (blink) drawCursor(); lastTextAt = now; lastReveal = reveal; lastBlink = blink; textDirty = true; };\n applyStyle();\n return {\n resize,\n render(now: number) {\n const options = getOptions(), requested = CRT_STYLES[options.variant] ? options.variant : \"terminal\";\n if (requested !== variant) { variant = requested; style = crtStyle(variant); applyStyle(); typed = 0; done = false; lastReveal = -1; lastBlink = -1; lastTextAt = 0; resize(); }\n const seconds = (now - startedAt) * 0.001 * options.speed;\n if (variant === \"terminal\") { if (!done) { typed += 4.4 * options.typeSpeed; if (typed >= TOTAL) { typed = TOTAL; done = true; } } maybeRedrawText(now); }\n else if (now - lastTextAt >= style.redrawMs || textDirty) { CRT_SCREENS[variant](textContext, width, height, seconds); lastTextAt = now; textDirty = true; }\n if (textDirty) uploadTexture();\n gl.useProgram(program); gl.uniform1f(uTime, seconds); gl.uniform1f(uMotion, options.motion); gl.drawArrays(gl.TRIANGLES, 0, 3);\n },\n dispose() { gl.deleteBuffer(buffer); gl.deleteTexture(texture); gl.deleteProgram(program); gl.deleteShader(vertex); gl.deleteShader(fragment); },\n };\n}\n" }, { "path": "src/shaders/crt/crtShaders.ts", "language": "typescript", "role": "shader-source", - "bytes": 2223, - "lines": 53, - "sha256": "7c71c67cf999527f2c3bfcc66b3dc81fb542625ddd7c06f547a4fa8854f1d127", - "code": "export const CRT_VERTEX_SHADER = \"attribute vec2 aPos;\\nvoid main(){ gl_Position = vec4(aPos,0.0,1.0); }\";\n\nexport const CRT_FRAGMENT_SHADER =\n \"precision highp float;\\n\"+\n \"uniform sampler2D uTex;\\n\"+\n \"uniform vec2 uRes;\\n\"+\n \"uniform float uTime;\\n\"+\n \"uniform float uMotion;\\n\"+\n \"float hash(vec2 p){ p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); }\\n\"+\n \"vec2 curve(vec2 uv){\\n\"+\n \" uv = uv*2.0-1.0;\\n\"+\n \" vec2 o = uv.yx*uv.yx;\\n\"+\n \" uv += uv * o * vec2(0.115,0.165);\\n\"+\n \" uv = uv*0.5+0.5;\\n\"+\n \" return uv;\\n\"+\n \"}\\n\"+\n \"void main(){\\n\"+\n \" vec2 fuv = gl_FragCoord.xy / uRes;\\n\"+\n \" vec2 uv = curve(fuv);\\n\"+\n \" vec2 inb = step(vec2(0.0), uv) * step(uv, vec2(1.0));\\n\"+\n \" float inside = inb.x*inb.y;\\n\"+\n \" vec2 ed = min(uv, 1.0-uv);\\n\"+\n \" inside *= smoothstep(0.0,0.020, min(ed.x,ed.y));\\n\"+\n \" vec2 dir = uv-0.5;\\n\"+\n \" float d2 = dot(dir,dir);\\n\"+\n \" vec2 ao = dir * (0.0016 + 0.012*d2);\\n\"+\n \" vec3 col;\\n\"+\n \" col.r = texture2D(uTex, uv + ao).r;\\n\"+\n \" col.g = texture2D(uTex, uv).g;\\n\"+\n \" col.b = texture2D(uTex, uv - ao).b;\\n\"+\n \" float lines = uRes.y*0.92;\\n\"+\n \" float sl = sin(uv.y*3.14159265*lines + uTime*4.0*uMotion);\\n\"+\n \" col *= mix(0.70,1.0, sl*sl);\\n\"+\n \" float gx = gl_FragCoord.x * (6.2831853/3.0);\\n\"+\n \" vec3 grille = 0.66 + 0.34*cos(gx + vec3(0.0,2.094,4.188));\\n\"+\n \" col *= grille;\\n\"+\n \" col *= 1.34;\\n\"+\n \" float bar = fract(uv.y*0.5 - uTime*0.07*uMotion);\\n\"+\n \" bar = smoothstep(0.0,0.05,bar)*smoothstep(0.18,0.05,bar);\\n\"+\n \" col += bar*0.045*uMotion;\\n\"+\n \" float sheen = smoothstep(0.55,0.0, distance(uv, vec2(0.50,0.15)));\\n\"+\n \" col += sheen*0.030*vec3(0.55,1.0,0.78);\\n\"+\n \" float vig = smoothstep(0.98,0.30, length((uv-0.5)*vec2(1.05,1.0)));\\n\"+\n \" col *= mix(0.42,1.0, vig);\\n\"+\n \" col *= 1.0 - 0.028*uMotion*sin(uTime*8.0);\\n\"+\n \" col += (hash(fuv + fract(uTime*0.37)) - 0.5)*0.022;\\n\"+\n \" float spill = smoothstep(0.85,0.18, length(fuv-0.5))*0.05;\\n\"+\n \" vec3 room = vec3(0.012,0.03,0.022) + vec3(0.0,spill*0.6,spill*0.42);\\n\"+\n \" col = mix(room, col, inside);\\n\"+\n \" col = max(col, vec3(0.004,0.010,0.008));\\n\"+\n \" gl_FragColor = vec4(col,1.0);\\n\"+\n \"}\";\n" + "bytes": 3800, + "lines": 116, + "sha256": "cf3a7c747d1cac495c705529954e2491ad885489ddd5110724f8f4b3553f1592", + "code": "export const CRT_VERTEX_SHADER = \"attribute vec2 aPos;\\nvoid main(){ gl_Position = vec4(aPos,0.0,1.0); }\";\n\nexport const CRT_FRAGMENT_SHADER = `precision highp float;\nuniform sampler2D uTex;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uMotion;\nuniform vec2 uCurve;\nuniform float uScan;\nuniform float uScanDepth;\nuniform float uTriad;\nuniform float uGrille;\nuniform float uChroma;\nuniform float uBar;\nuniform float uFlicker;\nuniform float uGrain;\nuniform float uNoise;\nuniform float uVignette;\nuniform float uMono;\nuniform float uGain;\nuniform float uHalo;\nuniform vec3 uSheen;\nuniform vec3 uRoom;\n\nfloat hash(vec2 p){ p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); }\n\nvec2 curve(vec2 uv){\n uv = uv*2.0-1.0;\n vec2 o = uv.yx*uv.yx;\n uv += uv * o * uCurve;\n uv = uv*0.5+0.5;\n return uv;\n}\n\nvoid main(){\n vec2 fuv = gl_FragCoord.xy / uRes;\n vec2 uv = curve(fuv);\n float t = uTime;\n\n /* analogue transport faults: per-row jitter, a rolling dropout band, and the\n head-switching scramble along the bottom edge of the raster */\n float band = 0.0;\n if (uNoise > 0.001){\n float row = floor(uv.y * 190.0);\n float gate = step(0.905, hash(vec2(row, floor(t*15.0))));\n uv.x += (hash(vec2(row*1.7, floor(t*15.0)+7.0)) - 0.5) * 0.052 * gate * uNoise;\n float pos = fract(uv.y * 0.8 - t * 0.17);\n band = smoothstep(0.075, 0.0, pos);\n uv.x += band * (hash(vec2(floor(uv.y*260.0), floor(t*26.0))) - 0.5) * 0.030 * uNoise;\n float head = smoothstep(0.030, 0.0, uv.y);\n uv.x += head * (hash(vec2(floor(uv.y*520.0), floor(t*22.0))) - 0.32) * 0.075 * uNoise;\n band = max(band, head);\n }\n\n vec2 inb = step(vec2(0.0), uv) * step(uv, vec2(1.0));\n float inside = inb.x*inb.y;\n vec2 ed = min(uv, 1.0-uv);\n inside *= smoothstep(0.0,0.020, min(ed.x,ed.y));\n\n vec2 dir = uv-0.5;\n float d2 = dot(dir,dir);\n vec2 ao = dir * (0.0010 + 0.0075*d2) * uChroma;\n vec3 col;\n col.r = texture2D(uTex, uv + ao).r;\n col.g = texture2D(uTex, uv).g;\n col.b = texture2D(uTex, uv - ao).b;\n\n /* phosphor halation: a wide cheap tap ring so bright glyphs bloom into the\n glass instead of relying on the text canvas alone */\n if (uHalo > 0.001){\n float s = 0.0038;\n vec3 wide = texture2D(uTex, uv + vec2( s, 0.0)).rgb\n + texture2D(uTex, uv + vec2(-s, 0.0)).rgb\n + texture2D(uTex, uv + vec2(0.0, s)).rgb\n + texture2D(uTex, uv + vec2(0.0, -s)).rgb\n + texture2D(uTex, uv + vec2( s, s)*0.72).rgb\n + texture2D(uTex, uv + vec2(-s, -s)*0.72).rgb;\n col += wide * (uHalo / 6.0);\n }\n\n float sl = sin(uv.y*3.14159265*uScan + t*4.0*uMotion);\n col *= mix(1.0 - uScanDepth, 1.0, sl*sl);\n\n float gx = gl_FragCoord.x * (6.2831853/max(uTriad, 1.0));\n vec3 grille = (1.0-uGrille) + uGrille*cos(gx + vec3(0.0,2.094,4.188));\n col *= mix(vec3(1.0), grille, step(0.001, uGrille));\n col *= uGain;\n\n float bar = fract(uv.y*0.5 - t*0.07*uMotion);\n bar = smoothstep(0.0,0.05,bar)*smoothstep(0.18,0.05,bar);\n col += bar*uBar*uMotion;\n\n float sheen = smoothstep(0.55,0.0, distance(uv, vec2(0.50,0.15)));\n col += sheen*0.030*uSheen;\n\n float vig = smoothstep(0.98,0.30, length((uv-0.5)*vec2(1.05,1.0)));\n col *= mix(1.0-uVignette, 1.0, vig);\n col *= 1.0 - uFlicker*uMotion*sin(t*8.0);\n\n if (uNoise > 0.001){\n float st = hash(fuv*uRes*0.5 + vec2(floor(t*24.0), floor(t*24.0)*1.7));\n col += (st-0.5)*0.135*uNoise;\n col += band*0.085*uNoise;\n }\n col += (hash(fuv + fract(t*0.37)) - 0.5)*uGrain;\n\n float luma = dot(col, vec3(0.2126,0.7152,0.0722));\n col = mix(col, vec3(luma), uMono);\n\n float spill = smoothstep(0.85,0.18, length(fuv-0.5))*0.05;\n vec3 room = uRoom + uSheen*spill*0.42;\n col = mix(room, col, inside);\n col = max(col, uRoom*0.34);\n gl_FragColor = vec4(col,1.0);\n}`;\n" + }, + { + "path": "src/shaders/crt/crtScreens.ts", + "language": "typescript", + "role": "variant-renderer-source", + "bytes": 22165, + "lines": 397, + "sha256": "e545922e0d3afa19b9d01840d0ea684c56d0799714f9cf77a4712921bfec7adb", + "code": "/* Screen painters for the CRT variants. Each one draws a complete picture into an\n offscreen 2D canvas that the WebGL pass then curves, scans, and grades. The\n authored ZION terminal stays in crtRenderer.ts; these are the added styles. */\n\nexport type CrtVariant = \"terminal\" | \"cinematic\" | \"blue-screen\" | \"nintendo\";\n\nexport type ScreenSurface =\n | { mode: \"buffer\" }\n | { mode: \"cap\"; width: number }\n | { mode: \"fixed\"; width: number; height: number };\n\nexport type CrtStyle = {\n curve: readonly [number, number];\n scanDensity: number;\n scanDepth: number;\n triadCss: number;\n grille: number;\n chroma: number;\n bar: number;\n flicker: number;\n grain: number;\n noise: number;\n vignette: number;\n mono: number;\n gain: number;\n halo: number;\n sheen: readonly [number, number, number];\n room: readonly [number, number, number];\n background: string;\n filtering: \"linear\" | \"nearest\";\n surface: ScreenSurface;\n redrawMs: number;\n};\n\nexport const CRT_STYLES: Record = {\n terminal: {\n curve: [0.115, 0.165], scanDensity: 0.44, scanDepth: 0.30, triadCss: 3.2, grille: 0.34, chroma: 1,\n bar: 0.045, flicker: 0.028, grain: 0.022, noise: 0, vignette: 0.58, mono: 0, gain: 1.34, halo: 0.10,\n sheen: [0.55, 1.0, 0.78], room: [0.012, 0.03, 0.022], background: \"#03100a\",\n filtering: \"linear\", surface: { mode: \"buffer\" }, redrawMs: 0,\n },\n cinematic: {\n curve: [0.085, 0.125], scanDensity: 0.40, scanDepth: 0.22, triadCss: 3.6, grille: 0.14, chroma: 0.7,\n bar: 0.022, flicker: 0.020, grain: 0.055, noise: 0, vignette: 0.74, mono: 1, gain: 1.16, halo: 0.20,\n sheen: [0.86, 0.90, 1.0], room: [0.016, 0.016, 0.018], background: \"#07070a\",\n filtering: \"linear\", surface: { mode: \"cap\", width: 1280 }, redrawMs: 33,\n },\n \"blue-screen\": {\n curve: [0.130, 0.180], scanDensity: 0.46, scanDepth: 0.34, triadCss: 3.0, grille: 0.30, chroma: 1.9,\n bar: 0.055, flicker: 0.042, grain: 0.038, noise: 1, vignette: 0.60, mono: 0, gain: 1.22, halo: 0.16,\n sheen: [0.62, 0.76, 1.0], room: [0.014, 0.020, 0.046], background: \"#050a24\",\n filtering: \"linear\", surface: { mode: \"cap\", width: 1600 }, redrawMs: 96,\n },\n nintendo: {\n curve: [0.070, 0.100], scanDensity: 0.34, scanDepth: 0.26, triadCss: 3.4, grille: 0.20, chroma: 0.55,\n bar: 0.018, flicker: 0.014, grain: 0.014, noise: 0, vignette: 0.46, mono: 0, gain: 1.20, halo: 0.06,\n sheen: [0.72, 0.84, 1.0], room: [0.020, 0.024, 0.040], background: \"#0a1030\",\n filtering: \"nearest\", surface: { mode: \"fixed\", width: 320, height: 180 }, redrawMs: 16,\n },\n};\n\nexport type ScreenPainter = (context: CanvasRenderingContext2D, width: number, height: number, time: number) => void;\n\nconst MONO_STACK = 'ui-monospace, \"SF Mono\", \"JetBrains Mono\", Menlo, Consolas, monospace';\nconst GROTESQUE_STACK = '\"Helvetica Neue\", \"Inter\", Helvetica, Arial, sans-serif';\nconst pad = (value: number, size = 2) => String(Math.floor(value)).padStart(size, \"0\");\n\n/* ---------------------------------------------------------------- cinematic */\n\nconst CINEMATIC_CYCLE = 8;\nconst CINEMATIC_CHROME = [\n { text: \"PICTURE START\", corner: \"tl\" }, { text: \"MONO · ACADEMY\", corner: \"bl\" }, { text: \"REEL 02 OF 04\", corner: \"br\" },\n] as const;\n\nfunction registrationMark(context: CanvasRenderingContext2D, x: number, y: number, size: number) {\n context.beginPath(); context.moveTo(x - size, y); context.lineTo(x + size, y); context.moveTo(x, y - size); context.lineTo(x, y + size); context.stroke();\n context.beginPath(); context.arc(x, y, size * 0.52, 0, Math.PI * 2); context.stroke();\n}\n\nconst paintCinematic: ScreenPainter = (context, width, height, time) => {\n const bar = height * 0.112, top = bar, bottom = height - bar, frameHeight = bottom - top;\n const cx = width / 2, cy = top + frameHeight / 2, radius = frameHeight * 0.325;\n const phase = ((time % CINEMATIC_CYCLE) + CINEMATIC_CYCLE) % CINEMATIC_CYCLE, counting = phase < 7;\n const label = Math.max(2, 9 - Math.ceil(phase || 0.0001));\n\n const wash = context.createLinearGradient(0, top, 0, bottom);\n wash.addColorStop(0, \"#101013\"); wash.addColorStop(0.55, \"#08080a\"); wash.addColorStop(1, \"#0d0d10\");\n context.setTransform(1, 0, 0, 1, 0, 0); context.fillStyle = wash; context.fillRect(0, 0, width, height);\n\n /* the running film edge: perforations scroll past on both sides of the frame */\n context.fillStyle = \"rgba(236,236,240,0.20)\";\n const pitch = frameHeight / 9, offset = (time * pitch * 2.4) % pitch, perfWidth = width * 0.011, perfHeight = pitch * 0.34;\n for (let y = top - pitch + offset; y < bottom + pitch; y += pitch) {\n context.fillRect(width * 0.022, y, perfWidth, perfHeight);\n context.fillRect(width - width * 0.022 - perfWidth, y, perfWidth, perfHeight);\n }\n\n context.strokeStyle = \"rgba(238,238,244,0.16)\"; context.lineWidth = Math.max(1, height * 0.0016);\n context.beginPath(); context.moveTo(cx, top); context.lineTo(cx, bottom); context.moveTo(width * 0.06, cy); context.lineTo(width * 0.94, cy); context.stroke();\n\n context.strokeStyle = \"rgba(238,238,244,0.30)\";\n for (const corner of [\"tl\", \"tr\", \"bl\", \"br\"] as const) {\n const x = corner.endsWith(\"l\") ? width * 0.085 : width * 0.915, y = corner.startsWith(\"t\") ? top + frameHeight * 0.16 : bottom - frameHeight * 0.16;\n registrationMark(context, x, y, height * 0.024);\n }\n\n if (counting) {\n context.strokeStyle = \"rgba(240,240,246,0.42)\"; context.lineWidth = Math.max(1.4, height * 0.0032);\n context.beginPath(); context.arc(cx, cy, radius, 0, Math.PI * 2); context.stroke();\n context.strokeStyle = \"rgba(240,240,246,0.22)\";\n context.beginPath(); context.arc(cx, cy, radius * 0.845, 0, Math.PI * 2); context.stroke();\n\n const sweep = (phase % 1) * Math.PI * 2, start = -Math.PI / 2;\n context.fillStyle = \"rgba(244,244,250,0.085)\";\n context.beginPath(); context.moveTo(cx, cy); context.arc(cx, cy, radius, start, start + sweep); context.closePath(); context.fill();\n context.strokeStyle = \"rgba(248,248,252,0.70)\"; context.lineWidth = Math.max(1.2, height * 0.0026);\n context.beginPath(); context.moveTo(cx, cy); context.lineTo(cx + Math.cos(start + sweep) * radius, cy + Math.sin(start + sweep) * radius); context.stroke();\n\n context.strokeStyle = \"rgba(238,238,244,0.34)\"; context.lineWidth = Math.max(1, height * 0.0020);\n for (let tick = 0; tick < 12; tick += 1) {\n const angle = start + (tick / 12) * Math.PI * 2, inner = tick % 3 === 0 ? radius * 1.055 : radius * 1.028;\n context.beginPath(); context.moveTo(cx + Math.cos(angle) * inner, cy + Math.sin(angle) * inner); context.lineTo(cx + Math.cos(angle) * radius * 1.10, cy + Math.sin(angle) * radius * 1.10); context.stroke();\n }\n\n context.textAlign = \"center\"; context.textBaseline = \"middle\";\n context.shadowColor = \"rgba(255,255,255,0.55)\"; context.shadowBlur = height * 0.030;\n context.fillStyle = \"#f6f6fa\"; context.font = `700 ${(radius * 1.28).toFixed(2)}px ${GROTESQUE_STACK}`;\n context.fillText(String(label), cx, cy + radius * 0.02);\n context.shadowBlur = 0;\n } else {\n const flash = Math.max(0, 1 - (phase - 7) / 0.10);\n if (flash > 0) { context.fillStyle = `rgba(250,250,252,${(flash * 0.62).toFixed(3)})`; context.fillRect(0, top, width, frameHeight); }\n context.textAlign = \"center\"; context.textBaseline = \"middle\";\n context.fillStyle = \"rgba(244,244,248,0.92)\";\n const size = height * 0.052;\n context.font = `500 ${size.toFixed(2)}px ${MONO_STACK}`;\n const title = \"T H E L O N G Q U I E T\";\n context.shadowColor = \"rgba(255,255,255,0.45)\"; context.shadowBlur = height * 0.020;\n /* clear of the horizontal crosshair, which otherwise rules through the baseline */\n context.fillText(title, cx, cy - size * 0.78);\n context.shadowBlur = 0;\n context.font = `400 ${(size * 0.42).toFixed(2)}px ${MONO_STACK}`;\n context.fillStyle = \"rgba(232,232,238,0.60)\";\n context.fillText(\"S C E N E 1 4 · T A K E 0 3\", cx, cy + size * 0.62);\n }\n\n const chromeSize = height * 0.0255;\n context.font = `500 ${chromeSize.toFixed(2)}px ${MONO_STACK}`; context.textBaseline = \"middle\"; context.fillStyle = \"rgba(226,226,232,0.66)\";\n for (const item of CINEMATIC_CHROME) {\n context.textAlign = item.corner.endsWith(\"l\") ? \"left\" : \"right\";\n const x = item.corner.endsWith(\"l\") ? width * 0.055 : width * 0.945;\n context.fillText(item.text, x, item.corner.startsWith(\"t\") ? top + frameHeight * 0.055 : bottom - frameHeight * 0.055);\n }\n const frames = Math.floor(time * 24);\n context.textAlign = \"right\";\n context.fillStyle = \"rgba(240,240,246,0.82)\";\n context.fillText(`01:${pad((frames / 1440) % 60)}:${pad((frames / 24) % 60)}:${pad(frames % 24)}`, width * 0.945, top + frameHeight * 0.055);\n\n context.fillStyle = \"#000\"; context.fillRect(0, 0, width, bar); context.fillRect(0, bottom, width, bar + 1);\n};\n\n/* -------------------------------------------------------------- blue screen */\n\ntype BlueLine = { text: string; tone?: \"head\" | \"dim\" | \"bright\" };\nconst BLUE_LINES: BlueLine[] = [\n { text: \"SIGNAL HALTED\", tone: \"head\" }, { text: \"\" },\n { text: \"A fault was detected in the video subsystem and the raster\" },\n { text: \"driver was stopped to prevent damage to the display.\" }, { text: \"\" },\n { text: \"* If this screen appears again, power the unit down and let\" },\n { text: \" the flyback transformer discharge before restarting.\" }, { text: \"\" },\n { text: \"* Horizontal deflection module HD-04 reported a bad sync\" },\n { text: \" pulse on line 312 of field 2.\" }, { text: \"\" },\n { text: \"Technical information:\", tone: \"bright\" }, { text: \"\" },\n { text: \"*** STOP: 0x0000CA7E (0x0F13D0C0, 0x00000002, 0xC0000005)\" },\n { text: \"*** RASTER.SYS - address 8C1FA00E base at 8C1F0000\" }, { text: \"\" },\n];\n\nconst paintBlueScreen: ScreenPainter = (context, width, height, time) => {\n const wash = context.createLinearGradient(0, 0, 0, height);\n wash.addColorStop(0, \"#212ec0\"); wash.addColorStop(0.62, \"#1a22a4\"); wash.addColorStop(1, \"#141a86\");\n context.setTransform(1, 0, 0, 1, 0, 0); context.fillStyle = wash; context.fillRect(0, 0, width, height);\n\n /* the panel is sized off the line budget so the whole fault report clears the\n curved edges of the tube at any aspect */\n const columns = 62, total = BLUE_LINES.length + 4;\n const size = Math.min((height * 0.88) / (total * 1.44), (width * 0.82) / (columns * 0.60));\n context.font = `600 ${size.toFixed(2)}px ${MONO_STACK}`;\n const advance = context.measureText(\"M\").width || size * 0.6, lineHeight = size * 1.44;\n const blockWidth = advance * columns, left = Math.round((width - blockWidth) / 2);\n const startY = Math.round((height - total * lineHeight) / 2);\n context.textBaseline = \"top\"; context.textAlign = \"left\";\n\n const head = BLUE_LINES[0].text, headWidth = advance * (head.length + 4);\n context.fillStyle = \"#e9ecff\";\n context.fillRect(Math.round((width - headWidth) / 2), startY - size * 0.20, headWidth, lineHeight);\n context.fillStyle = \"#161d92\";\n context.fillText(head, Math.round((width - headWidth) / 2) + advance * 2, startY);\n\n context.shadowColor = \"rgba(196,214,255,0.55)\"; context.shadowBlur = size * 0.30;\n let y = startY + lineHeight;\n for (const line of BLUE_LINES.slice(1)) {\n if (line.text) {\n context.fillStyle = line.tone === \"bright\" ? \"#ffffff\" : line.tone === \"dim\" ? \"#aab6f0\" : \"#dfe5ff\";\n context.fillText(line.text, left, y);\n }\n y += lineHeight;\n }\n\n const dump = Math.min(100, Math.floor((((time % 12) + 12) % 12) * 22));\n context.fillStyle = \"#dfe5ff\";\n context.fillText(dump >= 100 ? \"Dump of video memory complete.\" : `Beginning dump of video memory: ${pad(dump, 2)}%`, left, y);\n y += lineHeight * 2;\n const prompt = \"Press any key to restart the deflection stage \";\n context.fillText(prompt, left, y);\n if (Math.floor(time * 2) % 2 === 0) context.fillRect(left + advance * prompt.length, y + size * 0.08, advance * 0.9, size * 0.96);\n context.shadowBlur = 0;\n};\n\n/* ----------------------------------------------------------------- nintendo */\n\nconst GLYPHS: Record = {\n \"0\": \".###.#...##..###.#.###..##...#.###.\", \"1\": \"..#...##....#....#....#....#...###.\", \"2\": \".###.#...#....#...#...#...#...#####\",\n \"3\": \"####.....#....#.###.....#....#####.\", \"4\": \"#..#.#..#.#..#.#####...#....#....#.\", \"5\": \"######....####.....#....##...#.###.\",\n \"6\": \".###.#....#....####.#...##...#.###.\", \"7\": \"#####....#...#...#...#....#....#...\", \"8\": \".###.#...##...#.###.#...##...#.###.\",\n \"9\": \".###.#...##...#.####....#....#.###.\", A: \".###.#...##...#######...##...##...#\", B: \"####.#...##...#####.#...##...#####.\",\n C: \".#####....#....#....#....#.....####\", D: \"####.#...##...##...##...##...#####.\", E: \"######....#....####.#....#....#####\",\n F: \"######....#....####.#....#....#....\", G: \".#####....#....#..###...##...#.####\", H: \"#...##...##...#######...##...##...#\",\n I: \"#####..#....#....#....#....#..#####\", J: \"....#....#....#....##...##...#.###.\", K: \"#...##..#.#.#..##...#.#..#..#.#...#\",\n L: \"#....#....#....#....#....#....#####\", M: \"#...###.###.#.##...##...##...##...#\", N: \"#...###..##.#.##..###...##...##...#\",\n O: \".###.#...##...##...##...##...#.###.\", P: \"####.#...##...#####.#....#....#....\", Q: \".###.#...##...##...##.#.##..#..##.#\",\n R: \"####.#...##...#####.#.#..#..#.#...#\", S: \".#####....#.....###.....#....#####.\", T: \"#####..#....#....#....#....#....#..\",\n U: \"#...##...##...##...##...##...#.###.\", V: \"#...##...##...##...##...#.#.#...#..\", W: \"#...##...##...##...##.#.###.###...#\",\n X: \"#...##...#.#.#...#...#.#.#...##...#\", Y: \"#...##...#.#.#...#....#....#....#..\", Z: \"#####....#...#...#...#...#....#####\",\n \" \": \"...................................\", \"-\": \"...............#####...............\", \".\": \"..........................##...##..\",\n \":\": \"......##...##........##...##.......\", \"!\": \"..#....#....#....#....#.........#..\", \"?\": \".###.#...#....#..##...#.........#..\",\n \"(\": \"..##..#....#....#....#....#.....##.\", \")\": \".##.....#....#....#....#....#..##..\", \"/\": \"....#....#...#...#...#...#....#....\",\n \"*\": \".....#.#.#.###.#####.###.#.#.#.....\", \"'\": \"..#....#...........................\",\n};\n\nconst GLYPH_WIDTH = 5, GLYPH_HEIGHT = 7;\n\nfunction pixelTextWidth(text: string, scale: number) {\n return text.length ? (text.length * (GLYPH_WIDTH + 1) - 1) * scale : 0;\n}\n\nfunction drawPixelText(context: CanvasRenderingContext2D, text: string, x: number, y: number, scale: number, color: string) {\n context.fillStyle = color;\n let cursor = x;\n for (const character of text.toUpperCase()) {\n const rows = GLYPHS[character];\n if (rows) {\n for (let row = 0; row < GLYPH_HEIGHT; row += 1) {\n for (let column = 0; column < GLYPH_WIDTH; column += 1) {\n if (rows[row * GLYPH_WIDTH + column] === \"#\") context.fillRect(cursor + column * scale, y + row * scale, scale, scale);\n }\n }\n }\n cursor += (GLYPH_WIDTH + 1) * scale;\n }\n}\n\nconst RUNNER_PALETTE: Record = { \"1\": \"#1a1028\", \"2\": \"#e0402c\", \"3\": \"#f4f4f4\", \"4\": \"#2ec4e8\", \"5\": \"#22304a\" };\nconst RUNNER_HEAD = [\n \"....111111....\", \"...13333331...\", \"..1333333331..\", \"..1355555531..\", \"..1355555531..\", \"..1333333331..\", \"...13333331...\", \"....111111....\",\n \"...12222221...\", \".122222222221.\", \".124444444421.\", \".122222222221.\", \"..1222222221..\",\n];\nconst RUNNER_LEGS: Record = {\n a: [\"...122..221...\", \"...122..221...\", \"..1111..1111..\"],\n b: [\"..122....221..\", \".122......221.\", \"1111......1111\"],\n jump: [\"..122....221..\", \".1221....1221.\", \".111......111.\"],\n};\n\nfunction drawRunner(context: CanvasRenderingContext2D, x: number, y: number, pose: keyof typeof RUNNER_LEGS, flip: boolean) {\n const rows = [...RUNNER_HEAD, ...RUNNER_LEGS[pose]];\n for (let row = 0; row < rows.length; row += 1) {\n const line = rows[row];\n for (let column = 0; column < line.length; column += 1) {\n const color = RUNNER_PALETTE[line[column]];\n if (color) { context.fillStyle = color; context.fillRect(x + (flip ? line.length - 1 - column : column), y + row, 1, 1); }\n }\n }\n}\n\nfunction drawCloud(context: CanvasRenderingContext2D, x: number, y: number, scale: number) {\n context.fillStyle = \"#f4f8ff\";\n const puffs: readonly (readonly [number, number, number, number])[] = [[0, 4, 22, 6], [4, 1, 14, 4], [11, 2, 12, 5], [2, 8, 20, 3]];\n for (const [dx, dy, w, h] of puffs) context.fillRect(Math.round(x + dx * scale), Math.round(y + dy * scale), Math.round(w * scale), Math.round(h * scale));\n}\n\nfunction drawHill(context: CanvasRenderingContext2D, x: number, baseY: number, size: number, color: string) {\n context.fillStyle = color;\n for (let step = 0; step < size; step += 1) {\n const width = (size - step) * 4;\n context.fillRect(Math.round(x - width / 2), baseY - (step + 1) * 3, width, 3);\n }\n}\n\n/* one 320x180 title screen, laid out so the wordmark, credit, prompt, crates, and\n the runner's jump arc each own a horizontal band and never overlap */\nconst NINTENDO_CYCLE = 10;\nconst CRATE_X = [118, 208], GROUND_ROW = 154, JUMP_SPAN = 50, JUMP_LIFT = 22;\n\nfunction drawCrate(context: CanvasRenderingContext2D, x: number, y: number) {\n context.fillStyle = \"#20140c\"; context.fillRect(x, y, 16, 16);\n context.fillStyle = \"#d8902c\"; context.fillRect(x + 1, y + 1, 14, 14);\n context.fillStyle = \"#f0c060\"; context.fillRect(x + 1, y + 1, 14, 3);\n context.fillStyle = \"#8a5414\"; context.fillRect(x + 1, y + 11, 14, 3);\n context.fillStyle = \"#20140c\";\n context.fillRect(x + 6, y + 5, 4, 2); context.fillRect(x + 5, y + 7, 6, 2); context.fillRect(x + 6, y + 9, 4, 2);\n}\n\nfunction drawGem(context: CanvasRenderingContext2D, x: number, y: number, spin: number) {\n const gemWidth = [8, 6, 2, 6][spin], half = gemWidth / 2, inset = gemWidth > 3 ? 1 : 0;\n context.fillStyle = \"#20140c\";\n context.fillRect(x - half - 1, y + 1, gemWidth + 2, 8); context.fillRect(x - half, y - 1, gemWidth, 12);\n context.fillStyle = \"#ffe070\";\n context.fillRect(x - half, y + 1, gemWidth, 8); context.fillRect(x - half + inset, y, gemWidth - inset * 2, 10);\n context.fillStyle = \"#fff8c8\"; context.fillRect(x - half + inset, y + 2, Math.max(1, half - inset), 4);\n}\n\nconst paintNintendo: ScreenPainter = (context, width, height, time) => {\n const groundY = GROUND_ROW, phase = ((time % NINTENDO_CYCLE) + NINTENDO_CYCLE) % NINTENDO_CYCLE;\n\n const sky = context.createLinearGradient(0, 0, 0, groundY);\n sky.addColorStop(0, \"#2440b8\"); sky.addColorStop(0.55, \"#5c94fc\"); sky.addColorStop(1, \"#9ecbff\");\n context.setTransform(1, 0, 0, 1, 0, 0); context.fillStyle = sky; context.fillRect(0, 0, width, height);\n\n for (let star = 0; star < 22; star += 1) {\n const x = (star * 61) % width, y = 2 + ((star * 29) % 16);\n context.fillStyle = (star + Math.floor(time * 3)) % 5 === 0 ? \"rgba(255,255,255,0.85)\" : \"rgba(255,255,255,0.30)\";\n context.fillRect(x, y, 1, 1);\n }\n\n /* clouds sit in the sky band above the wordmark, never behind its counters */\n for (const [speed, cloudY, scale] of [[6, 18, 1.25], [3.5, 32, 0.85]] as const) {\n const span = width + 80;\n for (let copy = 0; copy < 3; copy += 1) drawCloud(context, (((time * speed) + (copy * span) / 3) % span) - 50, cloudY + (copy % 2) * 5, scale);\n }\n\n drawHill(context, 48, groundY, 9, \"#2f7a3a\");\n drawHill(context, 268, groundY, 11, \"#2f7a3a\");\n drawHill(context, 160, groundY, 6, \"#3e9a48\");\n\n context.fillStyle = \"#3ca03c\"; context.fillRect(0, groundY, width, 4);\n context.fillStyle = \"#2c7a2c\"; context.fillRect(0, groundY + 4, width, 2);\n context.fillStyle = \"#a05a28\"; context.fillRect(0, groundY + 6, width, height - groundY - 6);\n context.fillStyle = \"#7c4018\";\n for (let y = groundY + 6; y < height; y += 6) {\n context.fillRect(0, y, width, 1);\n for (let x = (y % 12 === 0 ? 0 : 6); x < width; x += 12) context.fillRect(x, y, 1, 6);\n }\n\n const spin = Math.floor(time * 8) % 4, bob = [0, 1, 1, 0][Math.floor(time * 6) % 4];\n for (const crateX of CRATE_X) { drawCrate(context, crateX, groundY - 16); drawGem(context, crateX + 8, groundY - 32 - bob, spin); }\n\n const runX = Math.round(-24 + ((phase / NINTENDO_CYCLE) * (width + 60)));\n let lift = 0;\n for (const crateX of CRATE_X) {\n const progress = (runX - (crateX - 28)) / JUMP_SPAN;\n if (progress > 0 && progress < 1) lift = Math.max(lift, Math.sin(progress * Math.PI) * JUMP_LIFT);\n }\n const pose = lift > 0.5 ? \"jump\" : Math.floor(time * 9) % 2 === 0 ? \"a\" : \"b\";\n context.fillStyle = \"rgba(20,16,10,0.20)\"; context.fillRect(runX + 2, groundY - 1, 10, 2);\n drawRunner(context, runX, groundY - 16 - Math.round(lift), pose, false);\n\n for (const [text, x] of [[\"PLAYER-1\", 10], [\"GEMS 0\" + (2 + Math.floor(phase / 4)), 96], [\"WORLD 1-1\", 174], [\"TIME \" + pad(Math.max(0, 384 - Math.floor(time * 2)) % 1000, 3), 254]] as const) {\n drawPixelText(context, String(text), Number(x), 7, 1, \"#141428\");\n drawPixelText(context, String(text), Number(x), 6, 1, \"#ffffff\");\n }\n\n const title = \"RASTER RUN\", titleWidth = pixelTextWidth(title, 3), titleX = Math.round((width - titleWidth) / 2);\n drawPixelText(context, title, titleX + 3, 53, 3, \"#141028\");\n drawPixelText(context, title, titleX, 50, 3, \"#f8e038\");\n /* a highlight band sweeps the wordmark the way an attract-mode title does */\n const sweepX = titleX + ((time * 130) % (titleWidth + 110)) - 55;\n context.save();\n context.beginPath(); context.rect(sweepX, 50, 24, 21); context.clip();\n drawPixelText(context, title, titleX, 50, 3, \"#fffce0\");\n context.restore();\n\n const credit = \"(C) 1987 THREEUI\", creditX = Math.round((width - pixelTextWidth(credit, 1)) / 2);\n drawPixelText(context, credit, creditX, 79, 1, \"#0e1430\");\n drawPixelText(context, credit, creditX, 78, 1, \"#dfe8ff\");\n\n if (Math.floor(time * 1.6) % 2 === 0) {\n const start = \"PUSH START\", startX = Math.round((width - pixelTextWidth(start, 2)) / 2);\n drawPixelText(context, start, startX + 2, 100, 2, \"#141028\");\n drawPixelText(context, start, startX, 98, 2, \"#ffffff\");\n }\n};\n\nexport const CRT_SCREENS: Record, ScreenPainter> = {\n cinematic: paintCinematic,\n \"blue-screen\": paintBlueScreen,\n nintendo: paintNintendo,\n};\n" } ], "assets": [] @@ -1635,6 +1644,15 @@ "src/shaders/community.css" ], "files": [ + { + "path": "src/shaders/woven-cloth/WovenCloth.tsx", + "language": "tsx", + "role": "component", + "bytes": 2993, + "lines": 95, + "sha256": "5a89ff035bdf33dbc642d2916b56dbe94e89cb0af184474c139ffbfe5a720550", + "code": "import { useMemo } from \"react\";\n\nimport {\n NEUFORM_CRAFT_DEFAULTS,\n WovenCloth as WovenClothSource,\n type NeuformCraftEffectProps,\n} from \"../neuform-isolated/NeuformCraftEffects\";\nimport atelierSource from \"./woven-cloth-atelier.html?raw\";\nimport iridescentSource from \"./woven-cloth-iridescent.html?raw\";\nimport washiSource from \"./woven-cloth-washi.html?raw\";\n\nexport const WOVEN_CLOTH_VARIANTS = [\"woven-cloth\", \"iridescent\", \"atelier\", \"washi\"] as const;\nexport type WovenClothVariant = (typeof WOVEN_CLOTH_VARIANTS)[number];\n\nexport type WovenClothProps = NeuformCraftEffectProps & {\n variant?: WovenClothVariant;\n};\n\n/* \"woven-cloth\" is the packaged Neuform export, rendered by its own isolated\n host so the authored document stays exactly as supplied. The other three are\n complete first-party documents: each keeps the parent's Verlet sheet and its\n habit of printing the wordmark into the textile, and rebuilds the material,\n the construction and the light around a different cloth. */\ntype CompanionDefinition = {\n title: string;\n background: string;\n source: string;\n};\n\nconst COMPANIONS: Record, CompanionDefinition> = {\n iridescent: {\n title: \"Woven Cloth iridescent silk\",\n background: \"#05060d\",\n source: iridescentSource,\n },\n atelier: {\n title: \"Woven Cloth atelier flag\",\n background: \"#12100d\",\n source: atelierSource,\n },\n washi: {\n title: \"Woven Cloth washi noren\",\n background: \"#0d0a07\",\n source: washiSource,\n },\n};\n\nfunction clamp(value: number, minimum: number, maximum: number) {\n return Math.min(maximum, Math.max(minimum, value));\n}\n\nfunction CompanionCloth({\n definition,\n hue = NEUFORM_CRAFT_DEFAULTS.hue,\n saturation = NEUFORM_CRAFT_DEFAULTS.saturation,\n brightness = NEUFORM_CRAFT_DEFAULTS.brightness,\n className,\n style,\n}: NeuformCraftEffectProps & { definition: CompanionDefinition }) {\n const safeHue = clamp(hue, -180, 180);\n const safeSaturation = clamp(saturation, 0, 2);\n const safeBrightness = clamp(brightness, 0.35, 1.65);\n const filter = safeHue === 0 && safeSaturation === 1 && safeBrightness === 1\n ? undefined\n : `hue-rotate(${safeHue}deg) saturate(${safeSaturation}) brightness(${safeBrightness})`;\n\n return (\n \n );\n}\n\nexport function WovenCloth({ variant = \"woven-cloth\", ...props }: WovenClothProps) {\n const definition = useMemo(\n () => (variant === \"woven-cloth\" ? undefined : COMPANIONS[variant] ?? undefined),\n [variant],\n );\n if (!definition) return ;\n return ;\n}\n" + }, { "path": "src/shaders/neuform-isolated/sources/lumina-weavers-cloth.html", "language": "html", @@ -1643,6 +1661,33 @@ "lines": 370, "sha256": "9bfd56ef7579a92cb6385b3e93866bc3ff54fa4489a0febb9809b720e2946fb6", "code": "\n\n\n \n \n Lumina Weavers · Kinetic Textiles, Kyoto\n \n \n \n \n \n\n\n\n \n
\n\n \n
\n \n
\n
\n\n \n
\n \n \n
\n
\n
\n Est. 2024 · Kyoto, Japan\n
\n
\n \n
\n\n \n
\n \n
\n
\n

\n Kinetic textiles, rendered to order.\n Every meter is simulated on bespoke kinetic engines. Digital threads, authentic physics, designed for infinite virtual environments.\n

\n
\n \n \n
\n\n
\n
\n Vector & matrix
\n No. 42 azure thread
\n Node 7 · Grid 2024\n
\n
\n\n
\n
\n\n \n\n" + }, + { + "path": "src/shaders/woven-cloth/woven-cloth-iridescent.html", + "language": "html", + "role": "variant-source", + "bytes": 21728, + "lines": 552, + "sha256": "e3b14adac39dfef04ed0bb0df99e86a1aa0aaf7cea4f8ecc4d5e0931b48bee7b", + "code": "\n\n\n\n\nWoven Cloth · Iridescent Silk\n\n\n\n\n\n
\n\n\n\n" + }, + { + "path": "src/shaders/woven-cloth/woven-cloth-atelier.html", + "language": "html", + "role": "variant-source", + "bytes": 18979, + "lines": 488, + "sha256": "f9be15756ff385db9cd3b7082b139d10b84a4eba0b3c4f19749b305570a7191f", + "code": "\n\n\n\n\nWoven Cloth · Atelier Flag\n\n\n\n\n\n
\n\n\n\n" + }, + { + "path": "src/shaders/woven-cloth/woven-cloth-washi.html", + "language": "html", + "role": "variant-source", + "bytes": 18332, + "lines": 482, + "sha256": "00e5971f139e5427e56a062c12d7e8e3590938b9a400753693b360d1e4d1a5c1", + "code": "\n\n\n\n\nWoven Cloth · Washi Noren\n\n\n\n\n\n
\n\n\n\n" } ], "assets": [] @@ -1817,7 +1862,7 @@ "id": "animated-top-dock", "exportName": "AnimatedTopDock", "sourceCommit": "5a736cd3c1f6f19802f61ebb10e1701b9f7aa26e", - "runtime": "DOM + CSS", + "runtime": "DOM + CSS + WebGL + Three.js r128", "sharedFilePaths": [ "src/shaders/community.css" ], @@ -1826,19 +1871,37 @@ "path": "src/shaders/animated-top-dock/AnimatedTopDock.tsx", "language": "tsx", "role": "component", - "bytes": 3149, - "lines": 60, - "sha256": "2782c74837d1a4d336decffb0391baabf77693413fe5d89efbcf3f09d4a04365", - "code": "import { useEffect, useRef, useState } from \"react\";\nimport { createTopDockController } from \"./topDockController\";\n\nexport type AnimatedTopDockProps = {\n proximity?: number;\n spring?: number;\n damping?: number;\n widthGrowth?: number;\n heightGrowth?: number;\n drop?: number;\n className?: string;\n};\n\nexport const ANIMATED_TOP_DOCK_DEFAULTS = {\n proximity: 122,\n spring: 0.19,\n damping: 0.7,\n widthGrowth: 17,\n heightGrowth: 16,\n drop: 3.5,\n} as const;\n\nconst ITEMS = [\n { id: \"system\", label: \"SYSTEM\", icon: <> },\n { id: \"method\", label: \"METHOD\", icon: <> },\n { id: \"work\", label: \"WORK\", icon: <> },\n { id: \"access\", label: \"ACCESS\", icon: <> },\n { id: \"notes\", label: \"NOTES\", icon: <> },\n] as const;\n\nexport function AnimatedTopDock({ className = \"\", ...props }: AnimatedTopDockProps) {\n const rootRef = useRef(null);\n const optionsRef = useRef({ ...ANIMATED_TOP_DOCK_DEFAULTS, ...props });\n optionsRef.current = { ...ANIMATED_TOP_DOCK_DEFAULTS, ...props };\n const [active, setActive] = useState(\"system\");\n\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return undefined;\n return createTopDockController(root, () => optionsRef.current);\n }, []);\n\n return (\n
\n \n

MOVE ACROSS THE DOCK · FOCUS WITH TAB

\n
\n );\n}\n" + "bytes": 16200, + "lines": 340, + "sha256": "50ddbba7ebc81565f42bd14dda34efb45e7b18c1aa47e97f196256ffedb4478f", + "code": "import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { createTopDockController, type TopDockOptions } from \"./topDockController\";\n\nexport const ANIMATED_TOP_DOCK_VARIANTS = [\"sable\", \"modern\", \"retro\", \"glass\"] as const;\nexport type AnimatedTopDockVariant = (typeof ANIMATED_TOP_DOCK_VARIANTS)[number];\n\nexport type AnimatedTopDockProps = {\n variant?: AnimatedTopDockVariant;\n proximity?: number;\n spring?: number;\n damping?: number;\n widthGrowth?: number;\n heightGrowth?: number;\n drop?: number;\n /* retro field */\n pixelSize?: number;\n speed?: number;\n noise?: number;\n levels?: number;\n scanlines?: number;\n /* glass field */\n particles?: number;\n thickness?: number;\n dispersion?: number;\n specular?: number;\n rim?: number;\n drift?: number;\n className?: string;\n};\n\nexport const ANIMATED_TOP_DOCK_DEFAULTS = {\n variant: \"sable\" as AnimatedTopDockVariant,\n proximity: 122,\n spring: 0.19,\n damping: 0.7,\n widthGrowth: 17,\n heightGrowth: 16,\n drop: 3.5,\n pixelSize: 4,\n speed: 1,\n noise: 1,\n levels: 7,\n scanlines: 0.32,\n particles: 22,\n thickness: 0.115,\n dispersion: 0.05,\n specular: 0.85,\n rim: 0.5,\n drift: 1,\n} as const;\n\ntype DockItem = { id: string; label: string; icon: ReactNode };\n\nconst ITEMS: readonly DockItem[] = [\n { id: \"system\", label: \"SYSTEM\", icon: <> },\n { id: \"method\", label: \"METHOD\", icon: <> },\n { id: \"work\", label: \"WORK\", icon: <> },\n { id: \"access\", label: \"ACCESS\", icon: <> },\n { id: \"notes\", label: \"NOTES\", icon: <> },\n];\n\nconst MODERN_ITEMS: readonly DockItem[] = [\n { id: \"product\", label: \"Product\", icon: <> },\n { id: \"solutions\", label: \"Solutions\", icon: <> },\n { id: \"docs\", label: \"Docs\", icon: <> },\n { id: \"pricing\", label: \"Pricing\", icon: <> },\n { id: \"changelog\", label: \"Changelog\", icon: <> },\n];\n\n/* every retro glyph is drawn on a 7x7 lattice of whole units so the icons stay\n on the same pixel grid as the dithered field behind them */\nconst RETRO_ITEMS: readonly DockItem[] = [\n { id: \"system\", label: \"SYSTEM\", icon: <> },\n { id: \"files\", label: \"FILES\", icon: <> },\n { id: \"net\", label: \"NET\", icon: <> },\n { id: \"disk\", label: \"DISK\", icon: <> },\n { id: \"help\", label: \"HELP\", icon: <> },\n];\n\nconst GLASS_ITEMS: readonly DockItem[] = [\n { id: \"overview\", label: \"Overview\", icon: <> },\n { id: \"studio\", label: \"Studio\", icon: <> },\n { id: \"library\", label: \"Library\", icon: <> },\n { id: \"motion\", label: \"Motion\", icon: <> },\n { id: \"labs\", label: \"Labs\", icon: <> },\n];\n\nconst VARIANT_ITEMS: Record = {\n sable: ITEMS,\n modern: MODERN_ITEMS,\n retro: RETRO_ITEMS,\n glass: GLASS_ITEMS,\n};\n\nconst BRAND_MARK = (\n \n \n \n \n \n \n);\n\nfunction useDockController(getOptions: () => TopDockOptions) {\n const rootRef = useRef(null);\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return undefined;\n return createTopDockController(root, getOptions);\n /* the getter is a stable ref reader, so the controller is built once */\n }, []);\n return rootRef;\n}\n\ntype ShaderField = {\n resize: (width: number, height: number) => void;\n render: (now: number) => void;\n setPointer?: (x: number, y: number) => void;\n dispose: () => void;\n};\n\n/* both shader variants share the same host lifecycle: measure from the shell,\n pause off-screen and on a hidden tab, and hand the renderer a css-pixel box.\n The module is resolved in its own effect so the effect that owns the GL\n context can create and tear it down synchronously — an async create survives\n its own cleanup and leaves two renderers fighting over one canvas. */\nfunction useShaderField(active: boolean, load: () => Promise<(canvas: HTMLCanvasElement) => ShaderField>) {\n const hostRef = useRef(null);\n const canvasRef = useRef(null);\n const [factory, setFactory] = useState<{ create: (canvas: HTMLCanvasElement) => ShaderField } | null>(null);\n\n useEffect(() => {\n if (!active) return undefined;\n let cancelled = false;\n load().then((create) => { if (!cancelled) setFactory({ create }); });\n return () => { cancelled = true; };\n }, [active]);\n\n useEffect(() => {\n const host = hostRef.current;\n const canvas = canvasRef.current;\n if (!active || !factory || !host || !canvas) return undefined;\n const field = factory.create(canvas);\n let frame = 0;\n let visible = true;\n let bounds = host.getBoundingClientRect();\n const resize = () => {\n bounds = host.getBoundingClientRect();\n field.resize(bounds.width, bounds.height);\n };\n const tick = (now: number) => {\n field.resize(bounds.width, bounds.height);\n field.render(now);\n frame = visible && !document.hidden ? requestAnimationFrame(tick) : 0;\n };\n const onPointerMove = (event: PointerEvent) => {\n field.setPointer?.(\n ((event.clientX - bounds.left) / Math.max(1, bounds.width)) * 2 - 1,\n -((((event.clientY - bounds.top) / Math.max(1, bounds.height)) * 2) - 1),\n );\n };\n const resizeObserver = new ResizeObserver(resize);\n const intersection = new IntersectionObserver(([entry]) => {\n visible = entry?.isIntersecting ?? true;\n if (visible && !frame) frame = requestAnimationFrame(tick);\n if (!visible && frame) { cancelAnimationFrame(frame); frame = 0; }\n });\n resizeObserver.observe(host);\n intersection.observe(host);\n host.addEventListener(\"pointermove\", onPointerMove, { passive: true });\n resize();\n frame = requestAnimationFrame(tick);\n return () => {\n if (frame) cancelAnimationFrame(frame);\n resizeObserver.disconnect();\n intersection.disconnect();\n host.removeEventListener(\"pointermove\", onPointerMove);\n field.dispose();\n };\n }, [active, factory]);\n\n return { hostRef, canvasRef };\n}\n\nexport function AnimatedTopDock({ className = \"\", ...props }: AnimatedTopDockProps) {\n const optionsRef = useRef({ ...ANIMATED_TOP_DOCK_DEFAULTS, ...props });\n optionsRef.current = { ...ANIMATED_TOP_DOCK_DEFAULTS, ...props };\n const variant = optionsRef.current.variant;\n const items = VARIANT_ITEMS[variant] ?? ITEMS;\n const [active, setActive] = useState(items[0].id);\n /* one spring, three fits: the command bar pins its track so a bar sized to its\n own content never moves, the terminal renormalises its cells across the\n strip, and the glass rail runs the proximity field down the y axis */\n const rootRef = useDockController(() => ({\n ...optionsRef.current,\n axis: variant === \"glass\" ? \"y\" as const : \"x\" as const,\n distribute: variant === \"retro\",\n lockTrack: variant === \"modern\",\n }));\n\n const retro = useShaderField(variant === \"retro\", async () => {\n const { createRetroPixelField } = await import(\"./retroPixelField\");\n return (canvas: HTMLCanvasElement) => createRetroPixelField(canvas, () => ({\n pixelSize: optionsRef.current.pixelSize,\n noise: optionsRef.current.noise,\n levels: optionsRef.current.levels,\n speed: optionsRef.current.speed,\n }));\n });\n const glass = useShaderField(variant === \"glass\", async () => {\n const { createGlassParticleField } = await import(\"./glassParticleField\");\n return (canvas: HTMLCanvasElement) => createGlassParticleField(canvas, () => ({\n count: optionsRef.current.particles,\n thickness: optionsRef.current.thickness,\n dispersion: optionsRef.current.dispersion,\n specular: optionsRef.current.specular,\n rim: optionsRef.current.rim,\n drift: optionsRef.current.drift,\n }));\n });\n\n const dockItems = (itemClass: string, iconClass: string, viewBox: string) => items.map((item) => (\n setActive(item.id)}\n >\n {item.icon}\n {item.label}\n \n ));\n\n if (variant === \"modern\") {\n return (\n
\n
\n
\n event.preventDefault()}>\n {BRAND_MARK}\n Lumina\n \n \n
\n \n \n
\n
\n
\n

Interface systems

\n

Everything above the fold

\n
\n

LOGO LEFT · DOCK CENTRE · ACTION RIGHT

\n
\n );\n }\n\n if (variant === \"retro\") {\n return (\n \n \n
\n
\n
\n \n \n \n SABLE//OS\n
\n \n \n
\n

\n MEM 640K\n DITHER 8×8\n PAL 8\n

\n

FITTED STRIP · ORDERED DITHER

\n
\n );\n }\n\n if (variant === \"glass\") {\n return (\n
\n \n
\n event.preventDefault()}>\n {BRAND_MARK}\n Aperture\n \n \n \n \n \n
\n

VERTICAL RAIL · SCREEN-SPACE DISPERSION

\n
\n );\n }\n\n return (\n
\n \n

MOVE ACROSS THE DOCK · FOCUS WITH TAB

\n
\n );\n}\n" }, { "path": "src/shaders/animated-top-dock/topDockController.ts", "language": "typescript", "role": "renderer-source", - "bytes": 6991, - "lines": 190, - "sha256": "12c96ea32b1ca7914e7bfbde2c8e8c8cde2f63f459c6b56a7fdb165be49ca39b", - "code": "export type TopDockOptions = {\n proximity: number;\n spring: number;\n damping: number;\n widthGrowth: number;\n heightGrowth: number;\n drop: number;\n};\n\ntype DockItemState = {\n element: HTMLElement;\n baseWidth: number;\n baseHeight: number;\n value: number;\n velocity: number;\n target: number;\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));\n\nexport function createTopDockController(\n root: HTMLElement,\n getOptions: () => TopDockOptions,\n) {\n const reducedQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const precisionQuery = window.matchMedia(\"(hover:hover) and (pointer:fine)\");\n const items: DockItemState[] = Array.from(root.querySelectorAll(\"[data-dock-item]\")).map((element) => ({\n element,\n baseWidth: 0,\n baseHeight: 0,\n value: 0,\n velocity: 0,\n target: 0,\n }));\n\n let enabled = false;\n let pointerActive = false;\n let dirty = false;\n let frame = 0;\n\n const canAnimate = () => !reducedQuery.matches && root.clientWidth > 0 && window.innerWidth > 600 && precisionQuery.matches;\n\n const measure = () => {\n enabled = canAnimate();\n for (const state of items) {\n state.element.style.width = \"\";\n state.element.style.height = \"\";\n state.element.style.transform = \"\";\n state.element.dataset.dockNear = \"false\";\n }\n for (const state of items) {\n const rect = state.element.getBoundingClientRect();\n state.baseWidth = rect.width;\n state.baseHeight = rect.height;\n state.value = 0;\n state.velocity = 0;\n state.target = 0;\n }\n pointerActive = false;\n dirty = false;\n root.dataset.dockState = enabled ? \"idle\" : \"static\";\n root.dataset.dockMax = \"0.00\";\n };\n\n const setTargets = (clientX: number) => {\n if (!enabled) return;\n const options = getOptions();\n const rects = items.map((state) => state.element.getBoundingClientRect());\n for (let index = 0; index < items.length; index += 1) {\n const center = rects[index].left + rects[index].width * 0.5;\n const proximity = clamp(1 - Math.abs(clientX - center) / Math.max(1, options.proximity), 0, 1);\n const influence = proximity * proximity * (3 - 2 * proximity);\n items[index].target = influence;\n items[index].element.dataset.dockNear = influence > 0.08 ? \"true\" : \"false\";\n }\n pointerActive = true;\n dirty = true;\n root.dataset.dockState = \"active\";\n };\n\n const focusItem = (item: HTMLElement) => {\n if (!enabled) return;\n const index = items.findIndex((state) => state.element === item);\n if (index < 0) return;\n items.forEach((state, itemIndex) => {\n state.target = itemIndex === index ? 1 : Math.abs(itemIndex - index) === 1 ? 0.24 : 0;\n state.element.dataset.dockNear = state.target > 0.08 ? \"true\" : \"false\";\n });\n pointerActive = false;\n dirty = true;\n root.dataset.dockState = \"focus\";\n };\n\n const reset = () => {\n pointerActive = false;\n dirty = true;\n items.forEach((state) => {\n state.target = 0;\n state.element.dataset.dockNear = \"false\";\n });\n };\n\n const draw = () => {\n if (enabled && dirty) {\n const options = getOptions();\n let moving = false;\n let maxValue = 0;\n for (const state of items) {\n state.velocity += (state.target - state.value) * options.spring;\n state.velocity *= options.damping;\n state.value += state.velocity;\n if (Math.abs(state.target - state.value) < 0.001 && Math.abs(state.velocity) < 0.001) {\n state.value = state.target;\n state.velocity = 0;\n } else {\n moving = true;\n }\n\n const value = clamp(state.value, 0, 1.08);\n const isLogo = state.element.classList.contains(\"animated-top-dock__logo\");\n const extraWidth = isLogo ? options.widthGrowth * (14 / 17) : Math.min(options.widthGrowth, state.baseWidth * 0.24);\n const extraHeight = isLogo ? options.heightGrowth * (14 / 16) : options.heightGrowth;\n state.element.style.width = `${(state.baseWidth + extraWidth * value).toFixed(2)}px`;\n state.element.style.height = `${(state.baseHeight + extraHeight * value).toFixed(2)}px`;\n state.element.style.transform = `translateY(${(value * options.drop).toFixed(2)}px)`;\n maxValue = Math.max(maxValue, value);\n }\n root.dataset.dockMax = maxValue.toFixed(2);\n if (!moving) {\n dirty = false;\n if (items.every((state) => state.target === 0)) root.dataset.dockState = \"idle\";\n }\n }\n frame = requestAnimationFrame(draw);\n };\n\n const onPointerMove = (event: PointerEvent) => setTargets(event.clientX);\n const onWindowPointerMove = (event: PointerEvent) => {\n if (!pointerActive) return;\n const rootRect = root.getBoundingClientRect();\n const itemRects = items.map((state) => state.element.getBoundingClientRect());\n const bottom = Math.max(rootRect.bottom, ...itemRects.map((rect) => rect.bottom));\n const outside = event.clientX < rootRect.left || event.clientX > rootRect.right || event.clientY < rootRect.top || event.clientY > bottom;\n if (outside) reset();\n };\n const onFocusIn = (event: FocusEvent) => {\n const item = (event.target as HTMLElement | null)?.closest(\"[data-dock-item]\");\n if (item) focusItem(item);\n };\n const onFocusOut = () => requestAnimationFrame(() => {\n if (!root.contains(document.activeElement)) reset();\n });\n const onKeyDown = (event: KeyboardEvent) => {\n const item = (event.target as HTMLElement | null)?.closest(\"[data-dock-item]\");\n if (item && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n item.click();\n }\n };\n const onClick = () => reset();\n\n const resizeObserver = new ResizeObserver(measure);\n resizeObserver.observe(root.parentElement ?? root);\n root.addEventListener(\"pointermove\", onPointerMove);\n root.addEventListener(\"pointerleave\", reset);\n root.addEventListener(\"focusin\", onFocusIn);\n root.addEventListener(\"focusout\", onFocusOut);\n root.addEventListener(\"keydown\", onKeyDown);\n root.addEventListener(\"click\", onClick);\n window.addEventListener(\"pointermove\", onWindowPointerMove, { passive: true });\n reducedQuery.addEventListener(\"change\", measure);\n precisionQuery.addEventListener(\"change\", measure);\n measure();\n frame = requestAnimationFrame(draw);\n\n return () => {\n cancelAnimationFrame(frame);\n resizeObserver.disconnect();\n root.removeEventListener(\"pointermove\", onPointerMove);\n root.removeEventListener(\"pointerleave\", reset);\n root.removeEventListener(\"focusin\", onFocusIn);\n root.removeEventListener(\"focusout\", onFocusOut);\n root.removeEventListener(\"keydown\", onKeyDown);\n root.removeEventListener(\"click\", onClick);\n window.removeEventListener(\"pointermove\", onWindowPointerMove);\n reducedQuery.removeEventListener(\"change\", measure);\n precisionQuery.removeEventListener(\"change\", measure);\n };\n}\n" + "bytes": 10664, + "lines": 257, + "sha256": "506ab23d4d42cf1b5bc89131e7714586ee107381bb18d9b0302766e9a1dee2bd", + "code": "export type TopDockAxis = \"x\" | \"y\";\n\nexport type TopDockOptions = {\n proximity: number;\n spring: number;\n damping: number;\n widthGrowth: number;\n heightGrowth: number;\n drop: number;\n /* \"y\" measures the proximity field down a vertical rail: items grow taller and\n lean out sideways instead of widening and dropping */\n axis?: TopDockAxis;\n /* the items own the whole track, so their widths are renormalised against it:\n opening one cell takes room from its neighbours and the strip stays exactly\n as wide as its container */\n distribute?: boolean;\n /* the opposite deal: the track is pinned to the width it has at rest and the\n magnified row is allowed to overflow it. A bar that hugs its own content\n needs this — without it the growing row widens the bar, which both slides\n the brand and the actions around and resizes the box this controller\n re-measures from, cancelling the spring on the next frame. */\n lockTrack?: boolean;\n};\n\ntype DockItemState = {\n element: HTMLElement;\n baseWidth: number;\n baseHeight: number;\n value: number;\n velocity: number;\n target: number;\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));\n\nexport function createTopDockController(\n root: HTMLElement,\n getOptions: () => TopDockOptions,\n) {\n const reducedQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const precisionQuery = window.matchMedia(\"(hover:hover) and (pointer:fine)\");\n const items: DockItemState[] = Array.from(root.querySelectorAll(\"[data-dock-item]\")).map((element) => ({\n element,\n baseWidth: 0,\n baseHeight: 0,\n value: 0,\n velocity: 0,\n target: 0,\n }));\n\n let enabled = false;\n let pointerActive = false;\n let dirty = false;\n let frame = 0;\n\n const canAnimate = () => !reducedQuery.matches && root.clientWidth > 0 && window.innerWidth > 600 && precisionQuery.matches;\n\n const measure = () => {\n enabled = canAnimate();\n /* released first, so the rest width is measured against the track's own\n content rather than against the width the last measurement pinned */\n if (getOptions().lockTrack) root.style.width = \"\";\n for (const state of items) {\n state.element.style.width = \"\";\n state.element.style.height = \"\";\n state.element.style.transform = \"\";\n state.element.dataset.dockNear = \"false\";\n }\n for (const state of items) {\n const rect = state.element.getBoundingClientRect();\n state.baseWidth = rect.width;\n state.baseHeight = rect.height;\n state.value = 0;\n state.velocity = 0;\n state.target = 0;\n }\n pointerActive = false;\n dirty = false;\n /* a distributed strip has to fill its track even when the spring never runs\n — reduced motion, a coarse pointer, or a narrow viewport */\n if (getOptions().distribute) applyLayout();\n if (getOptions().lockTrack) root.style.width = `${root.getBoundingClientRect().width.toFixed(2)}px`;\n root.dataset.dockState = enabled ? \"idle\" : \"static\";\n root.dataset.dockMax = \"0.00\";\n };\n\n const setTargets = (clientX: number, clientY: number) => {\n if (!enabled) return;\n const options = getOptions();\n const vertical = options.axis === \"y\";\n const pointer = vertical ? clientY : clientX;\n const rects = items.map((state) => state.element.getBoundingClientRect());\n for (let index = 0; index < items.length; index += 1) {\n const rect = rects[index];\n const center = vertical ? rect.top + rect.height * 0.5 : rect.left + rect.width * 0.5;\n const proximity = clamp(1 - Math.abs(pointer - center) / Math.max(1, options.proximity), 0, 1);\n const influence = proximity * proximity * (3 - 2 * proximity);\n items[index].target = influence;\n items[index].element.dataset.dockNear = influence > 0.08 ? \"true\" : \"false\";\n }\n pointerActive = true;\n dirty = true;\n root.dataset.dockState = \"active\";\n };\n\n const focusItem = (item: HTMLElement) => {\n if (!enabled) return;\n const index = items.findIndex((state) => state.element === item);\n if (index < 0) return;\n items.forEach((state, itemIndex) => {\n state.target = itemIndex === index ? 1 : Math.abs(itemIndex - index) === 1 ? 0.24 : 0;\n state.element.dataset.dockNear = state.target > 0.08 ? \"true\" : \"false\";\n });\n pointerActive = false;\n dirty = true;\n root.dataset.dockState = \"focus\";\n };\n\n const reset = () => {\n pointerActive = false;\n dirty = true;\n items.forEach((state) => {\n state.target = 0;\n state.element.dataset.dockNear = \"false\";\n });\n };\n\n /* the only place item geometry is written, so the three fits stay one\n behaviour with three ways of spending the same spring value */\n const applyLayout = () => {\n const options = getOptions();\n if (options.distribute && options.axis !== \"y\") {\n const weights = items.map((state) => state.baseWidth + options.widthGrowth * clamp(state.value, 0, 1.08));\n const total = weights.reduce((sum, weight) => sum + weight, 0);\n const natural = items.reduce((sum, state) => sum + state.baseWidth, 0);\n /* squeezed below its own natural width the strip would clip every label,\n so it stops filling the track rather than crushing the cells */\n const track = root.clientWidth >= natural ? root.clientWidth : 0;\n items.forEach((state, index) => {\n state.element.style.width = track ? `${(track * weights[index] / total).toFixed(2)}px` : \"\";\n state.element.style.height = \"\";\n state.element.style.transform = \"\";\n });\n return;\n }\n for (const state of items) {\n const value = clamp(state.value, 0, 1.08);\n if (options.axis === \"y\") {\n state.element.style.width = \"\";\n state.element.style.height = `${(state.baseHeight + options.heightGrowth * value).toFixed(2)}px`;\n state.element.style.transform = `translateX(${(value * options.drop).toFixed(2)}px)`;\n continue;\n }\n const isLogo = state.element.classList.contains(\"animated-top-dock__logo\");\n const extraWidth = isLogo ? options.widthGrowth * (14 / 17) : Math.min(options.widthGrowth, state.baseWidth * 0.24);\n const extraHeight = isLogo ? options.heightGrowth * (14 / 16) : options.heightGrowth;\n state.element.style.width = `${(state.baseWidth + extraWidth * value).toFixed(2)}px`;\n state.element.style.height = `${(state.baseHeight + extraHeight * value).toFixed(2)}px`;\n state.element.style.transform = `translateY(${(value * options.drop).toFixed(2)}px)`;\n }\n };\n\n const draw = () => {\n if (enabled && dirty) {\n const options = getOptions();\n let moving = false;\n let maxValue = 0;\n for (const state of items) {\n state.velocity += (state.target - state.value) * options.spring;\n state.velocity *= options.damping;\n state.value += state.velocity;\n if (Math.abs(state.target - state.value) < 0.001 && Math.abs(state.velocity) < 0.001) {\n state.value = state.target;\n state.velocity = 0;\n } else {\n moving = true;\n }\n maxValue = Math.max(maxValue, clamp(state.value, 0, 1.08));\n }\n applyLayout();\n root.dataset.dockMax = maxValue.toFixed(2);\n if (!moving) {\n dirty = false;\n if (items.every((state) => state.target === 0)) root.dataset.dockState = \"idle\";\n }\n }\n frame = requestAnimationFrame(draw);\n };\n\n const onPointerMove = (event: PointerEvent) => setTargets(event.clientX, event.clientY);\n const onWindowPointerMove = (event: PointerEvent) => {\n if (!pointerActive) return;\n const rootRect = root.getBoundingClientRect();\n const itemRects = items.map((state) => state.element.getBoundingClientRect());\n const bottom = Math.max(rootRect.bottom, ...itemRects.map((rect) => rect.bottom));\n const outside = event.clientX < rootRect.left || event.clientX > rootRect.right || event.clientY < rootRect.top || event.clientY > bottom;\n if (outside) reset();\n };\n const onFocusIn = (event: FocusEvent) => {\n const item = (event.target as HTMLElement | null)?.closest(\"[data-dock-item]\");\n if (item) focusItem(item);\n };\n const onFocusOut = () => requestAnimationFrame(() => {\n if (!root.contains(document.activeElement)) reset();\n });\n const onKeyDown = (event: KeyboardEvent) => {\n const item = (event.target as HTMLElement | null)?.closest(\"[data-dock-item]\");\n if (item && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n item.click();\n }\n };\n const onClick = () => reset();\n\n /* the spring writes an explicit pixel width onto every item, so the base\n measurement has to happen after the label font is available — measured\n against a fallback face, a variant with auto-width items locks in a box its\n own text then overflows */\n let released = false;\n const remeasure = () => { if (!released) measure(); };\n document.fonts?.ready.then(remeasure);\n\n /* re-measuring on the parent works for a dock inside a fixed bar, but a rail\n whose height follows its own items would resize its parent as it grows and\n cancel the spring on the next frame. A shell can opt out of that feedback\n loop by marking a box the dock cannot resize. */\n const resizeObserver = new ResizeObserver(measure);\n resizeObserver.observe(root.closest(\"[data-dock-frame]\") ?? root.parentElement ?? root);\n root.addEventListener(\"pointermove\", onPointerMove);\n root.addEventListener(\"pointerleave\", reset);\n root.addEventListener(\"focusin\", onFocusIn);\n root.addEventListener(\"focusout\", onFocusOut);\n root.addEventListener(\"keydown\", onKeyDown);\n root.addEventListener(\"click\", onClick);\n window.addEventListener(\"pointermove\", onWindowPointerMove, { passive: true });\n reducedQuery.addEventListener(\"change\", measure);\n precisionQuery.addEventListener(\"change\", measure);\n measure();\n frame = requestAnimationFrame(draw);\n\n return () => {\n released = true;\n root.style.width = \"\";\n cancelAnimationFrame(frame);\n resizeObserver.disconnect();\n root.removeEventListener(\"pointermove\", onPointerMove);\n root.removeEventListener(\"pointerleave\", reset);\n root.removeEventListener(\"focusin\", onFocusIn);\n root.removeEventListener(\"focusout\", onFocusOut);\n root.removeEventListener(\"keydown\", onKeyDown);\n root.removeEventListener(\"click\", onClick);\n window.removeEventListener(\"pointermove\", onWindowPointerMove);\n reducedQuery.removeEventListener(\"change\", measure);\n precisionQuery.removeEventListener(\"change\", measure);\n };\n}\n" + }, + { + "path": "src/shaders/animated-top-dock/retroPixelField.ts", + "language": "typescript", + "role": "shader-source", + "bytes": 7469, + "lines": 183, + "sha256": "54f0469be51dfce2da7ce37c952d42c583cc7996794bf96d1ea0b2bb35d94115", + "code": "export type RetroPixelOptions = {\n pixelSize: number;\n noise: number;\n levels: number;\n speed: number;\n};\n\nexport const RETRO_PIXEL_DEFAULTS: RetroPixelOptions = {\n pixelSize: 4,\n noise: 1,\n levels: 7,\n speed: 1,\n};\n\nconst VERTEX_SHADER = \"attribute vec2 aPos;\\nvoid main(){ gl_Position = vec4(aPos, 0.0, 1.0); }\";\n\n/* everything is quantised before it reaches a colour: the scene is authored as a\n single luminance field, dithered with an ordered 8x8 Bayer threshold, snapped\n to `uLevels` steps, and only then mapped through the eight-stop dusk ramp — so\n the bands are real palette steps rather than a gradient with noise on top */\nconst FRAGMENT_SHADER = `precision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uNoise;\nuniform float uLevels;\n\nfloat hash(vec2 p){ p = fract(p * vec2(127.1, 311.7)); p += dot(p, p + 34.23); return fract(p.x * p.y); }\n\nfloat vnoise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n f = f * f * (3.0 - 2.0 * f);\n float a = hash(i), b = hash(i + vec2(1.0, 0.0)), c = hash(i + vec2(0.0, 1.0)), d = hash(i + vec2(1.0, 1.0));\n return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);\n}\n\nfloat fbm(vec2 p){\n float sum = 0.0, amp = 0.5;\n for (int i = 0; i < 5; i++){ sum += amp * vnoise(p); p = p * 2.03 + 11.7; amp *= 0.5; }\n return sum;\n}\n\n/* the classic recursive Bayer construction: each level halves the cell and adds a\n quarter of the coarser threshold, which is cheaper than indexing a matrix and\n avoids the dynamic array access GLSL ES 1.0 will not guarantee */\nfloat bayer2(vec2 a){ a = floor(a); return fract(a.x * 0.5 + a.y * a.y * 0.75); }\nfloat bayer4(vec2 a){ return bayer2(a * 0.5) * 0.25 + bayer2(a); }\nfloat bayer8(vec2 a){ return bayer4(a * 0.5) * 0.25 + bayer2(a); }\n\nvec3 stop(float index){\n if (index < 0.5) return vec3(0.043, 0.035, 0.109);\n if (index < 1.5) return vec3(0.106, 0.063, 0.220);\n if (index < 2.5) return vec3(0.212, 0.090, 0.325);\n if (index < 3.5) return vec3(0.396, 0.129, 0.376);\n if (index < 4.5) return vec3(0.612, 0.180, 0.376);\n if (index < 5.5) return vec3(0.827, 0.298, 0.325);\n if (index < 6.5) return vec3(0.945, 0.502, 0.286);\n return vec3(0.988, 0.784, 0.494);\n}\n\nvoid main(){\n vec2 uv = gl_FragCoord.xy / uRes;\n float t = uTime;\n\n /* two counter-drifting noise sheets read as slow weather rather than as a\n single scrolling texture */\n vec2 cloud = vec2(uv.x * 3.4, uv.y * 2.1);\n float weather = fbm(cloud + vec2(t * 0.055, t * -0.021));\n weather = mix(weather, fbm(cloud * 1.9 + vec2(-t * 0.032, t * 0.044)), 0.42);\n\n /* the composition is three bands: a deep sky that warms downward, a hot\n horizon at y=0.30, and a dark ground the readout chrome can sit on. The\n falloffs are deliberately wide — a steep ramp stacks the palette steps into\n thin horizontal bars the ordered dither is too small to break up */\n float sky = smoothstep(0.98, 0.20, uv.y) * 0.36;\n float heat = smoothstep(0.60, 0.16, uv.y) * 0.32;\n float ground = smoothstep(0.26, 0.08, uv.y);\n float sun = smoothstep(0.38, 0.0, length((uv - vec2(0.5, 0.29)) * vec2(0.70, 1.5))) * 0.20;\n /* the three terms sum to just under 1.0 at the horizon: pushing past it clips\n the whole band to the last palette stop and the dither texture disappears */\n float field = 0.11 + sky + heat + sun - ground * 0.80 + (weather - 0.5) * 0.82 * uNoise;\n\n /* static lives before quantisation so it moves the pixel across a palette step\n instead of tinting it */\n float grain = hash(floor(gl_FragCoord.xy) + floor(t * 12.0)) - 0.5;\n field += grain * 0.055 * uNoise;\n field *= 1.0 - 0.34 * smoothstep(0.45, 1.05, length((uv - vec2(0.5, 0.46)) * vec2(1.06, 1.0)));\n\n float levels = max(uLevels, 2.0);\n float dither = bayer8(gl_FragCoord.xy) - 0.5;\n float quantised = clamp(field + dither / levels, 0.0, 0.9999);\n /* no scanline here: one drawing-buffer row is pixelSize screen rows tall,\n so darkening alternate rows draws chunky bars rather than a raster. The CRT\n line structure is a screen-resolution CSS overlay instead. */\n gl_FragColor = vec4(stop(floor(quantised * levels) * (7.0 / (levels - 1.0))), 1.0);\n}`;\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string) {\n const shader = gl.createShader(type);\n if (!shader) return null;\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.warn(\"Retro pixel field shader failed to compile.\", gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n}\n\nconst INERT = { resize: () => {}, render: () => {}, dispose: () => {} };\n\nexport function createRetroPixelField(canvas: HTMLCanvasElement, getOptions: () => RetroPixelOptions) {\n const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: false, depth: false, preserveDrawingBuffer: false });\n if (!gl) return INERT;\n\n const program = gl.createProgram();\n const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n if (!program || !vertex || !fragment) return INERT;\n gl.attachShader(program, vertex);\n gl.attachShader(program, fragment);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.warn(\"Retro pixel field program failed to link.\", gl.getProgramInfoLog(program));\n return INERT;\n }\n gl.useProgram(program);\n\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n const position = gl.getAttribLocation(program, \"aPos\");\n\n const uRes = gl.getUniformLocation(program, \"uRes\");\n const uTime = gl.getUniformLocation(program, \"uTime\");\n const uNoise = gl.getUniformLocation(program, \"uNoise\");\n const uLevels = gl.getUniformLocation(program, \"uLevels\");\n\n let width = 1;\n let height = 1;\n const startedAt = performance.now();\n let clock = 0;\n let lastAt = startedAt;\n\n const resize = (cssWidth: number, cssHeight: number) => {\n const options = getOptions();\n /* the drawing buffer is the low-resolution artwork; CSS blows it up with\n nearest-neighbour sampling, so every pixel stays square and hard-edged */\n width = Math.max(2, Math.round(cssWidth / Math.max(1, options.pixelSize)));\n height = Math.max(2, Math.round(cssHeight / Math.max(1, options.pixelSize)));\n if (canvas.width === width && canvas.height === height) return;\n canvas.width = width;\n canvas.height = height;\n gl.viewport(0, 0, width, height);\n };\n\n const render = (now = performance.now()) => {\n const options = getOptions();\n clock += Math.min(96, now - lastAt) * 0.001 * options.speed;\n lastAt = now;\n /* the attribute state is re-established every draw: this canvas can outlive\n one field instance, and a stale binding silently draws nothing */\n gl.useProgram(program);\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.enableVertexAttribArray(position);\n gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n gl.uniform2f(uRes, width, height);\n gl.uniform1f(uTime, clock);\n gl.uniform1f(uNoise, options.noise);\n gl.uniform1f(uLevels, options.levels);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n };\n\n const dispose = () => {\n gl.deleteBuffer(buffer);\n gl.deleteProgram(program);\n gl.deleteShader(vertex);\n gl.deleteShader(fragment);\n };\n\n return { resize, render, dispose };\n}\n" + }, + { + "path": "src/shaders/animated-top-dock/glassParticleField.ts", + "language": "typescript", + "role": "renderer-source", + "bytes": 11896, + "lines": 322, + "sha256": "bc9f07ae6da33b28f82303ea937095806f8e3a2ab08bd1301ae8457aaf0c3464", + "code": "import * as THREE from \"three128\";\n\nexport type GlassParticleOptions = {\n count: number;\n thickness: number;\n dispersion: number;\n specular: number;\n rim: number;\n drift: number;\n};\n\nexport const GLASS_PARTICLE_DEFAULTS: GlassParticleOptions = {\n count: 22,\n thickness: 0.115,\n dispersion: 0.05,\n specular: 0.85,\n rim: 0.5,\n drift: 1,\n};\n\nconst MAX_COUNT = 34;\n\nconst QUAD_VERTEX = \"void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }\";\n\n/* the backdrop is drawn twice each frame: once into the refraction target the\n glass samples, once straight to the canvas — one cheap fullscreen pass is far\n less work than a second material that blits the target back */\nconst BACKDROP_FRAGMENT = `precision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec2 uPointer;\n\nvec3 bloom(vec2 uv, vec2 centre, float radius, vec3 tint){\n float aspect = uRes.x / max(uRes.y, 1.0);\n float d = length((uv - centre) * vec2(aspect, 1.0));\n float falloff = smoothstep(radius, 0.0, d);\n return tint * falloff * falloff;\n}\n\nvoid main(){\n vec2 uv = gl_FragCoord.xy / uRes;\n float t = uTime * 0.17;\n /* the base tone matters more than the blooms: refraction samples the whole\n frame, so a backdrop with black regions makes beads read as dark holes */\n vec3 col = vec3(0.085, 0.095, 0.125);\n col += mix(vec3(0.0), vec3(0.05, 0.06, 0.10), uv.y);\n col += bloom(uv, vec2(0.23 + 0.055 * sin(t * 0.9), 0.76 + 0.045 * cos(t * 0.7)), 0.86, vec3(0.24, 0.42, 0.96)) * 0.92;\n col += bloom(uv, vec2(0.82 + 0.05 * cos(t * 0.8), 0.34 + 0.055 * sin(t * 1.1)), 0.80, vec3(0.66, 0.30, 0.88)) * 0.80;\n col += bloom(uv, vec2(0.50 + 0.07 * sin(t * 0.6 + 1.7), 0.10 + 0.04 * cos(t * 0.9)), 0.74, vec3(0.14, 0.68, 0.70)) * 0.60;\n col += bloom(uv, vec2(0.10 + 0.04 * cos(t * 1.2), 0.16 + 0.05 * sin(t * 0.8)), 0.58, vec3(0.98, 0.60, 0.40)) * 0.34;\n col += bloom(uv, vec2(0.5 + uPointer.x * 0.20, 0.56 + uPointer.y * 0.16), 0.46, vec3(0.74, 0.78, 0.96)) * 0.30;\n float vignette = smoothstep(1.34, 0.30, length((uv - 0.5) * vec2(1.05, 1.0)));\n col *= mix(0.62, 1.0, vignette);\n gl_FragColor = vec4(col, 1.0);\n}`;\n\nconst GLASS_VERTEX = `varying vec3 vNormalView;\nvarying vec3 vViewPos;\nvarying vec4 vScreen;\n\nvoid main(){\n vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);\n vNormalView = normalize(normalMatrix * normal);\n vViewPos = mvPosition.xyz;\n vScreen = projectionMatrix * mvPosition;\n gl_Position = vScreen;\n}`;\n\n/* screen-space refraction: the surface normal bends the view ray, the offset is\n applied to this fragment's own screen coordinate, and the three channels use\n three slightly different indices so the bead edges split into colour the way\n real glass does */\nconst GLASS_FRAGMENT = `precision highp float;\nuniform sampler2D uBackdrop;\nuniform float uThickness;\nuniform float uDispersion;\nuniform float uSpecular;\nuniform float uRim;\nuniform vec3 uTint;\nuniform vec3 uLight;\nvarying vec3 vNormalView;\nvarying vec3 vViewPos;\nvarying vec4 vScreen;\n\nvoid main(){\n vec2 screenUV = (vScreen.xy / vScreen.w) * 0.5 + 0.5;\n vec3 N = normalize(vNormalView);\n vec3 V = normalize(-vViewPos);\n vec3 I = -V;\n float fresnel = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 3.4);\n\n vec2 offsetR = refract(I, N, 1.0 / (1.44 - uDispersion)).xy * uThickness;\n vec2 offsetG = refract(I, N, 1.0 / 1.44).xy * uThickness;\n vec2 offsetB = refract(I, N, 1.0 / (1.44 + uDispersion)).xy * uThickness;\n\n vec3 col;\n col.r = texture2D(uBackdrop, clamp(screenUV + offsetR, 0.002, 0.998)).r;\n col.g = texture2D(uBackdrop, clamp(screenUV + offsetG, 0.002, 0.998)).g;\n col.b = texture2D(uBackdrop, clamp(screenUV + offsetB, 0.002, 0.998)).b;\n col *= uTint;\n\n vec3 L = normalize(uLight);\n vec3 H = normalize(L + V);\n float ndoth = max(dot(N, H), 0.0);\n col += pow(ndoth, 150.0) * uSpecular;\n col += pow(ndoth, 16.0) * uSpecular * 0.11;\n /* a second, dimmer key from below keeps the underside of every bead alive */\n vec3 H2 = normalize(normalize(vec3(0.55, -0.7, 0.45)) + V);\n col += pow(max(dot(N, H2), 0.0), 44.0) * uSpecular * 0.22;\n col += fresnel * uRim * vec3(0.86, 0.90, 1.0);\n\n /* aerial perspective: the small far beads dissolve into the plate they float\n over instead of reading as hard specks */\n float haze = smoothstep(7.4, 11.4, -vViewPos.z);\n col = mix(col, texture2D(uBackdrop, screenUV).rgb, haze * 0.7);\n gl_FragColor = vec4(col, 1.0);\n}`;\n\ntype Bead = {\n mesh: THREE.Mesh;\n material: THREE.ShaderMaterial;\n origin: THREE.Vector3;\n bob: number;\n phase: number;\n spin: THREE.Vector3;\n radius: number;\n};\n\n/* one deterministic sequence so the arrangement is identical on every load and a\n recorded preview matches what a visitor sees */\nfunction seeded(seed: number) {\n let state = seed;\n return () => {\n state = (state * 1664525 + 1013904223) % 4294967296;\n return state / 4294967296;\n };\n}\n\nexport function createGlassParticleField(canvas: HTMLCanvasElement, getOptions: () => GlassParticleOptions) {\n const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setClearColor(0x0a0b0f, 1);\n\n const backdropScene = new THREE.Scene();\n const backdropCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n const backdropUniforms = {\n uRes: { value: new THREE.Vector2(1, 1) },\n uTime: { value: 0 },\n uPointer: { value: new THREE.Vector2() },\n };\n const backdropMaterial = new THREE.ShaderMaterial({\n uniforms: backdropUniforms,\n vertexShader: QUAD_VERTEX,\n fragmentShader: BACKDROP_FRAGMENT,\n depthTest: false,\n depthWrite: false,\n });\n const backdropGeometry = new THREE.PlaneGeometry(2, 2);\n backdropScene.add(new THREE.Mesh(backdropGeometry, backdropMaterial));\n\n const target = new THREE.WebGLRenderTarget(2, 2, {\n minFilter: THREE.LinearFilter,\n magFilter: THREE.LinearFilter,\n format: THREE.RGBAFormat,\n });\n\n const scene = new THREE.Scene();\n const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);\n camera.position.set(0, 0, 7.4);\n const group = new THREE.Group();\n scene.add(group);\n\n const geometries = [\n new THREE.SphereGeometry(1, 44, 30),\n new THREE.IcosahedronGeometry(1, 1),\n new THREE.TorusGeometry(0.78, 0.30, 22, 56),\n ];\n const tints = [\n new THREE.Color(1.04, 1.0, 1.02),\n new THREE.Color(0.97, 1.0, 1.06),\n new THREE.Color(1.05, 0.99, 0.97),\n ];\n\n const random = seeded(20260826);\n const beads: Bead[] = [];\n for (let index = 0; index < MAX_COUNT; index += 1) {\n /* the first third are the large hero beads, the remainder are the fine dust\n that reads as depth behind them */\n const hero = index < 8;\n const radius = hero ? 0.40 + random() * 0.34 : 0.09 + random() * 0.18;\n /* spheres carry the look; the gem and the ring appear once each among the\n hero beads so the field has silhouette variety without reading as props */\n const geometry = geometries[hero ? (index === 3 ? 1 : index === 6 ? 2 : 0) : 0];\n const material = new THREE.ShaderMaterial({\n uniforms: {\n uBackdrop: { value: target.texture },\n uThickness: { value: 0.1 },\n uDispersion: { value: 0.05 },\n uSpecular: { value: 0.85 },\n uRim: { value: 0.5 },\n uTint: { value: tints[index % tints.length] },\n uLight: { value: new THREE.Vector3(-0.45, 0.86, 0.62) },\n },\n vertexShader: GLASS_VERTEX,\n fragmentShader: GLASS_FRAGMENT,\n });\n const mesh = new THREE.Mesh(geometry, material);\n mesh.scale.setScalar(radius);\n /* hero beads are rejection-sampled against the ones already placed: two\n large spheres that intersect read as one lumpy blob rather than as glass */\n const origin = new THREE.Vector3();\n for (let attempt = 0; attempt < 48; attempt += 1) {\n origin.set(\n (random() - 0.5) * 8.4,\n (random() - 0.5) * 5.0 - 0.25,\n hero ? -1.4 + random() * 2.6 : -3.6 + random() * 2.4,\n );\n if (!hero) break;\n const clear = beads.every((placed) => {\n const dx = placed.origin.x - origin.x;\n const dy = placed.origin.y - origin.y;\n return Math.hypot(dx, dy) > (placed.radius + radius) * 1.25 + 0.3;\n });\n if (clear) break;\n }\n mesh.position.copy(origin);\n mesh.rotation.set(random() * 6.28, random() * 6.28, random() * 6.28);\n group.add(mesh);\n beads.push({\n mesh,\n material,\n origin,\n radius,\n bob: 0.14 + random() * 0.34,\n phase: random() * 6.28,\n spin: new THREE.Vector3((random() - 0.5) * 0.28, (random() - 0.5) * 0.34, (random() - 0.5) * 0.2),\n });\n }\n\n const pointer = new THREE.Vector2();\n const pointerTarget = new THREE.Vector2();\n let width = 1;\n let height = 1;\n let clock = 0;\n let lastAt = performance.now();\n\n const resize = (cssWidth: number, cssHeight: number) => {\n const nextWidth = Math.max(1, Math.round(cssWidth));\n const nextHeight = Math.max(1, Math.round(cssHeight));\n if (nextWidth === width && nextHeight === height) return;\n width = nextWidth;\n height = nextHeight;\n const ratio = Math.min(window.devicePixelRatio, 2);\n renderer.setSize(width, height, false);\n target.setSize(Math.round(width * ratio), Math.round(height * ratio));\n backdropUniforms.uRes.value.set(width * ratio, height * ratio);\n camera.aspect = width / height;\n /* a wide catalog frame should widen the field of view rather than crop it, so\n the bead spread stays the same composition at every aspect */\n camera.fov = camera.aspect > 1 ? 42 : 42 / Math.max(0.62, camera.aspect);\n camera.updateProjectionMatrix();\n for (const bead of beads) bead.material.uniforms.uBackdrop.value = target.texture;\n };\n\n const setPointer = (x: number, y: number) => pointerTarget.set(x, y);\n\n const render = (now = performance.now()) => {\n const options = getOptions();\n clock += Math.min(96, now - lastAt) * 0.001;\n lastAt = now;\n pointer.lerp(pointerTarget, 0.045);\n\n const visible = Math.max(4, Math.min(MAX_COUNT, Math.round(options.count)));\n for (let index = 0; index < beads.length; index += 1) {\n const bead = beads[index];\n bead.mesh.visible = index < visible;\n if (!bead.mesh.visible) continue;\n const uniforms = bead.material.uniforms;\n uniforms.uThickness.value = options.thickness * (0.55 + bead.radius * 0.9);\n uniforms.uDispersion.value = options.dispersion;\n uniforms.uSpecular.value = options.specular;\n uniforms.uRim.value = options.rim;\n const t = clock * options.drift;\n bead.mesh.position.set(\n bead.origin.x + Math.sin(t * 0.21 + bead.phase) * bead.bob * 0.9,\n bead.origin.y + Math.cos(t * 0.27 + bead.phase * 1.3) * bead.bob,\n bead.origin.z + Math.sin(t * 0.17 + bead.phase * 0.7) * bead.bob * 0.5,\n );\n bead.mesh.rotation.x += bead.spin.x * 0.0075 * options.drift;\n bead.mesh.rotation.y += bead.spin.y * 0.0075 * options.drift;\n bead.mesh.rotation.z += bead.spin.z * 0.0075 * options.drift;\n }\n\n group.rotation.y = pointer.x * 0.14;\n group.rotation.x = -pointer.y * 0.1;\n group.position.x = pointer.x * 0.28;\n group.position.y = pointer.y * 0.2;\n\n backdropUniforms.uTime.value = clock;\n backdropUniforms.uPointer.value.set(pointer.x, pointer.y);\n\n renderer.setRenderTarget(target);\n renderer.render(backdropScene, backdropCamera);\n renderer.setRenderTarget(null);\n renderer.render(backdropScene, backdropCamera);\n renderer.autoClear = false;\n renderer.render(scene, camera);\n renderer.autoClear = true;\n };\n\n const dispose = () => {\n for (const bead of beads) bead.material.dispose();\n for (const geometry of geometries) geometry.dispose();\n backdropGeometry.dispose();\n backdropMaterial.dispose();\n target.dispose();\n renderer.dispose();\n };\n\n return { resize, render, setPointer, dispose };\n}\n" } ], "assets": [ @@ -2187,14 +2250,113 @@ }, { "id": "skeuomorphic-toggle", - "exportName": "SkeuomorphicToggle", + "exportName": "SkeuomorphicToggleCollection", "sourceCommit": "SHA-256 3e19e7fec9ac", - "runtime": "DOM/CSS", + "runtime": "DOM/CSS + Three.js + Raw WebGL", "sharedFilePaths": [ "src/shaders/neuform-isolated/NeuformBatchEffects.tsx", "src/shaders/community.css" ], "files": [ + { + "path": "src/shaders/skeuomorphic-toggle/SkeuomorphicToggleCollection.tsx", + "language": "tsx", + "role": "component", + "bytes": 2333, + "lines": 88, + "sha256": "944ee582c2578e470e98f83ea7931018a85b87e44198ef2819613d1a76643150", + "code": "import { lazy, Suspense, type CSSProperties } from \"react\";\n\nimport type { ToggleModePreference } from \"./toggleMode\";\n\nexport const SKEUOMORPHIC_TOGGLE_VARIANTS = [\n \"skeuomorphic-toggle\",\n \"modern\",\n \"glass\",\n \"shader\",\n] as const;\n\nexport type SkeuomorphicToggleVariant = (typeof SKEUOMORPHIC_TOGGLE_VARIANTS)[number];\n\nexport type SkeuomorphicToggleCollectionProps = {\n variant?: SkeuomorphicToggleVariant;\n mode?: ToggleModePreference;\n defaultOn?: boolean;\n label?: string;\n speed?: number;\n size?: number;\n opacity?: number;\n hue?: number;\n saturation?: number;\n brightness?: number;\n onChange?: (on: boolean) => void;\n className?: string;\n style?: CSSProperties;\n};\n\n/* The packaged Neuform export stays exactly as it shipped; the three companion\n treatments are first-party React renderers. */\nconst PackagedToggle = lazy(() =>\n import(\"../neuform-isolated/NeuformBatchEffects\").then((module) => ({ default: module.SkeuomorphicToggle })),\n);\n\nconst ModernToggle = lazy(() =>\n import(\"./ModernToggle\").then((module) => ({ default: module.ModernToggle })),\n);\n\nconst GlassToggle = lazy(() =>\n import(\"./GlassToggle\").then((module) => ({ default: module.GlassToggle })),\n);\n\nconst ShaderToggle = lazy(() =>\n import(\"./ShaderToggle\").then((module) => ({ default: module.ShaderToggle })),\n);\n\nconst FALLBACK =
;\n\nexport function SkeuomorphicToggleCollection({\n variant = \"skeuomorphic-toggle\",\n onChange,\n defaultOn,\n label,\n speed,\n ...props\n}: SkeuomorphicToggleCollectionProps) {\n if (variant === \"modern\") {\n return (\n \n \n \n );\n }\n\n if (variant === \"glass\") {\n return (\n \n \n \n );\n }\n\n if (variant === \"shader\") {\n return (\n \n \n \n );\n }\n\n return (\n \n \n \n );\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/ModernToggle.tsx", + "language": "tsx", + "role": "variant-component", + "bytes": 5394, + "lines": 166, + "sha256": "d6545c319a3123ac573756d0d3725e73c279143ea470a48256101222cb37fa16", + "code": "import { useCallback, useEffect, useRef, useState, type CSSProperties } from \"react\";\n\nimport \"./modern-toggle.css\";\nimport { clamp, useToggleMode, type ToggleModePreference } from \"./toggleMode\";\n\nexport type ModernToggleProps = {\n mode?: ToggleModePreference;\n defaultOn?: boolean;\n label?: string;\n size?: number;\n opacity?: number;\n hue?: number;\n saturation?: number;\n brightness?: number;\n onChange?: (on: boolean) => void;\n className?: string;\n style?: CSSProperties;\n};\n\nexport const MODERN_TOGGLE_DEFAULTS = {\n mode: \"auto\" as ToggleModePreference,\n defaultOn: true,\n label: \"Live Sync\",\n size: 1,\n opacity: 1,\n hue: 0,\n saturation: 1,\n brightness: 1,\n} as const;\n\nexport function ModernToggle({\n mode = MODERN_TOGGLE_DEFAULTS.mode,\n defaultOn = MODERN_TOGGLE_DEFAULTS.defaultOn,\n label = MODERN_TOGGLE_DEFAULTS.label,\n size = MODERN_TOGGLE_DEFAULTS.size,\n opacity = MODERN_TOGGLE_DEFAULTS.opacity,\n hue = MODERN_TOGGLE_DEFAULTS.hue,\n saturation = MODERN_TOGGLE_DEFAULTS.saturation,\n brightness = MODERN_TOGGLE_DEFAULTS.brightness,\n onChange,\n className,\n style,\n}: ModernToggleProps) {\n const resolvedMode = useToggleMode(mode);\n const [on, setOn] = useState(defaultOn);\n const switchRef = useRef(null);\n const thumbRef = useRef(null);\n\n /* A light spring rather than an eased transition: the knob leans into the\n travel, trails a little squash behind it, and settles with one small\n overshoot. Damping ratio ~0.67 — enough to read as a spring, well short\n of bouncy. */\n useEffect(() => {\n const control = switchRef.current;\n const thumb = thumbRef.current;\n if (!control || !thumb) return undefined;\n\n const reduceMotion = typeof window !== \"undefined\"\n && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n const target = on ? 1 : 0;\n let value = Number(thumb.dataset.progress ?? (on ? 1 : 0));\n let velocity = Number(thumb.dataset.velocity ?? 0);\n let handle = 0;\n let last = performance.now();\n\n const apply = (progress: number, speed: number) => {\n const travel = Math.max(0, control.clientWidth - thumb.offsetLeft * 2 - thumb.offsetWidth);\n /* Squash scales with how fast it is actually moving, and trails the\n direction of travel. */\n const lean = Math.min(1, Math.abs(speed) / 6);\n const scaleX = 1 + lean * 0.16;\n const scaleY = 1 - lean * 0.1;\n thumb.style.transformOrigin = speed >= 0 ? \"right center\" : \"left center\";\n thumb.style.transform =\n `translate3d(${(progress * travel).toFixed(2)}px, 0, 0) scale(${scaleX.toFixed(4)}, ${scaleY.toFixed(4)})`;\n };\n\n if (reduceMotion) {\n thumb.dataset.progress = String(target);\n thumb.dataset.velocity = \"0\";\n apply(target, 0);\n return undefined;\n }\n\n const step = (now: number) => {\n const delta = Math.min(0.032, (now - last) / 1000);\n last = now;\n const stiffness = 210;\n const damping = 19.5;\n velocity += ((target - value) * stiffness - velocity * damping) * delta;\n value += velocity * delta;\n thumb.dataset.progress = String(value);\n thumb.dataset.velocity = String(velocity);\n apply(value, velocity);\n if (Math.abs(target - value) < 0.0006 && Math.abs(velocity) < 0.006) {\n value = target;\n velocity = 0;\n thumb.dataset.progress = String(target);\n thumb.dataset.velocity = \"0\";\n apply(target, 0);\n return;\n }\n handle = window.requestAnimationFrame(step);\n };\n\n handle = window.requestAnimationFrame(step);\n return () => window.cancelAnimationFrame(handle);\n }, [on]);\n\n const flip = useCallback(() => {\n setOn((current) => {\n const next = !current;\n onChange?.(next);\n return next;\n });\n }, [onChange]);\n\n const stageStyle = {\n \"--modern-toggle-scale\": clamp(size, 0.35, 2.5),\n \"--modern-toggle-hue\": `${clamp(hue, -180, 180)}deg`,\n \"--modern-toggle-saturation\": clamp(saturation, 0, 2),\n \"--modern-toggle-brightness\": clamp(brightness, 0.35, 1.65),\n opacity: clamp(opacity, 0.05, 1),\n ...style,\n } as CSSProperties;\n\n return (\n \n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n

\n {label}\n {on ? \"On\" : \"Off\"}\n

\n
\n
\n );\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/modern-toggle.css", + "language": "css", + "role": "variant-style", + "bytes": 6316, + "lines": 217, + "sha256": "2748dc81ffd1f8506c56f6cd316a522df8f5f7bbeda751017a88efb2fc892674", + "code": ".modern-toggle,\n.modern-toggle *,\n.modern-toggle *::before,\n.modern-toggle *::after {\n box-sizing: border-box;\n}\n\n.modern-toggle {\n --modern-toggle-scale: 1;\n --modern-toggle-hue: 0deg;\n --modern-toggle-saturation: 1;\n --modern-toggle-brightness: 1;\n --modern-toggle-track: #e6e8ec;\n --modern-toggle-track-on: #2f6bff;\n --modern-toggle-edge: rgba(9, 12, 20, 0.09);\n --modern-toggle-edge-on: rgba(9, 12, 20, 0.06);\n --modern-toggle-thumb: #ffffff;\n --modern-toggle-thumb-shadow: 0 1px 1px rgba(9, 12, 20, 0.1), 0 6px 14px -4px rgba(9, 12, 20, 0.28);\n --modern-toggle-mark: #2f6bff;\n --modern-toggle-caption: rgba(15, 20, 32, 0.42);\n --modern-toggle-caption-on: rgba(15, 20, 32, 0.7);\n --modern-toggle-focus: rgba(47, 107, 255, 0.32);\n --modern-toggle-stage: #f7f8fa;\n --modern-toggle-stage-top: #ffffff;\n --modern-toggle-stage-bottom: #eceff4;\n --modern-toggle-halo: rgba(47, 107, 255, 0.1);\n position: relative;\n display: grid;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n place-items: center;\n overflow: hidden;\n isolation: isolate;\n container-type: size;\n background: linear-gradient(180deg, var(--modern-toggle-stage-top) 0%, var(--modern-toggle-stage) 62%, var(--modern-toggle-stage-bottom) 100%);\n color-scheme: light;\n filter: hue-rotate(var(--modern-toggle-hue)) saturate(var(--modern-toggle-saturation)) brightness(var(--modern-toggle-brightness));\n font-family: ui-sans-serif, -apple-system, \"SF Pro Text\", \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n}\n\n.modern-toggle[data-mode=\"dark\"] {\n --modern-toggle-track: #22262f;\n --modern-toggle-track-on: #3b7bff;\n --modern-toggle-edge: rgba(255, 255, 255, 0.1);\n --modern-toggle-edge-on: rgba(255, 255, 255, 0.16);\n --modern-toggle-thumb: #f2f4f8;\n --modern-toggle-thumb-shadow: 0 1px 1px rgba(0, 0, 0, 0.5), 0 8px 18px -6px rgba(0, 0, 0, 0.7);\n --modern-toggle-mark: #1f5cf0;\n --modern-toggle-caption: rgba(232, 237, 247, 0.4);\n --modern-toggle-caption-on: rgba(232, 237, 247, 0.74);\n --modern-toggle-focus: rgba(94, 145, 255, 0.4);\n --modern-toggle-stage: #0c0e13;\n --modern-toggle-stage-top: #14171f;\n --modern-toggle-stage-bottom: #06070a;\n --modern-toggle-halo: rgba(59, 123, 255, 0.16);\n color-scheme: dark;\n}\n\n/* One soft pool of accent light, so the switch is not floating on a flat fill. */\n.modern-toggle__halo {\n position: absolute;\n width: min(72cqw, 620px);\n height: min(72cqh, 380px);\n border-radius: 50%;\n background: radial-gradient(closest-side, var(--modern-toggle-halo), transparent 72%);\n opacity: 0;\n transform: scale(0.86);\n transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.32, 0.72, 0, 1);\n pointer-events: none;\n}\n\n.modern-toggle[data-state=\"on\"] .modern-toggle__halo {\n opacity: 1;\n transform: scale(1);\n}\n\n.modern-toggle__stack {\n position: relative;\n display: grid;\n gap: clamp(12px, 2.4cqh, 24px);\n justify-items: center;\n transform: scale(var(--modern-toggle-scale));\n}\n\n/* Every measurement below is a share of the switch's own width, so the control\n keeps its proportions at any stage size. */\n.modern-toggle__switch {\n position: relative;\n display: block;\n width: clamp(104px, min(18cqw, 33cqh), 330px);\n aspect-ratio: 44 / 25;\n padding: 0;\n border: 0;\n border-radius: 999px;\n background: transparent;\n cursor: pointer;\n container-type: inline-size;\n -webkit-tap-highlight-color: transparent;\n appearance: none;\n}\n\n.modern-toggle__switch:focus-visible {\n outline: none;\n}\n\n.modern-toggle__track {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background: var(--modern-toggle-track);\n box-shadow: inset 0 0 0 1px var(--modern-toggle-edge);\n transition: background 0.46s cubic-bezier(0.32, 0.72, 0, 1), box-shadow 0.46s ease;\n}\n\n.modern-toggle[data-state=\"on\"] .modern-toggle__track {\n background: var(--modern-toggle-track-on);\n box-shadow: inset 0 0 0 1px var(--modern-toggle-edge-on);\n}\n\n.modern-toggle__switch:focus-visible .modern-toggle__track {\n box-shadow: inset 0 0 0 1px var(--modern-toggle-edge), 0 0 0 4px var(--modern-toggle-focus);\n}\n\n.modern-toggle[data-state=\"on\"] .modern-toggle__switch:focus-visible .modern-toggle__track {\n box-shadow: inset 0 0 0 1px var(--modern-toggle-edge-on), 0 0 0 4px var(--modern-toggle-focus);\n}\n\n/* The knob's transform is driven by a spring in JS, not by a transition —\n a CSS transition cannot squash on the way and settle on arrival, so the\n stretch had to be an :active snap that fought the travel. */\n.modern-toggle__thumb {\n position: absolute;\n top: 4.6cqw;\n left: 4.6cqw;\n width: 47.6cqw;\n height: 47.6cqw;\n display: grid;\n place-items: center;\n border-radius: 999px;\n background: var(--modern-toggle-thumb);\n box-shadow: var(--modern-toggle-thumb-shadow);\n transform: translate3d(0, 0, 0);\n transform-origin: center;\n will-change: transform;\n}\n\n.modern-toggle__mark {\n position: relative;\n width: 36%;\n height: 36%;\n color: var(--modern-toggle-mark);\n}\n\n.modern-toggle__mark svg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n fill: none;\n stroke: currentColor;\n stroke-width: 2.6;\n stroke-linecap: round;\n stroke-linejoin: round;\n transition: opacity 0.28s ease, transform 0.34s cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.modern-toggle__mark svg[data-mark=\"check\"] {\n opacity: 0;\n transform: scale(0.6) rotate(-18deg);\n}\n\n.modern-toggle__mark svg[data-mark=\"dash\"] {\n opacity: 0.34;\n transform: scale(1);\n}\n\n.modern-toggle[data-state=\"on\"] .modern-toggle__mark svg[data-mark=\"check\"] {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n}\n\n.modern-toggle[data-state=\"on\"] .modern-toggle__mark svg[data-mark=\"dash\"] {\n opacity: 0;\n transform: scale(0.6);\n}\n\n.modern-toggle__caption {\n display: flex;\n align-items: baseline;\n gap: 0.55em;\n font-size: clamp(11px, 0.85cqw, 16px);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--modern-toggle-caption);\n transition: color 0.4s ease;\n}\n\n.modern-toggle[data-state=\"on\"] .modern-toggle__caption {\n color: var(--modern-toggle-caption-on);\n}\n\n.modern-toggle__caption b {\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modern-toggle__halo,\n .modern-toggle__track,\n .modern-toggle__mark svg,\n .modern-toggle__caption {\n transition-duration: 0.01ms;\n }\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/GlassToggle.tsx", + "language": "tsx", + "role": "variant-component", + "bytes": 5401, + "lines": 179, + "sha256": "406eda34475c2d87b699690097ae50e9e3064dedcacda4cabb3dd8c282122a9b", + "code": "import { useCallback, useEffect, useRef, useState, type CSSProperties, type PointerEvent } from \"react\";\n\nimport \"./glass-toggle.css\";\nimport { createGlassToggleScene, type GlassToggleScene } from \"./glassToggleScene\";\nimport { clamp, useToggleMode, type ToggleModePreference } from \"./toggleMode\";\n\nexport type GlassToggleProps = {\n mode?: ToggleModePreference;\n defaultOn?: boolean;\n label?: string;\n speed?: number;\n size?: number;\n opacity?: number;\n hue?: number;\n saturation?: number;\n brightness?: number;\n onChange?: (on: boolean) => void;\n className?: string;\n style?: CSSProperties;\n};\n\nexport const GLASS_TOGGLE_DEFAULTS = {\n mode: \"auto\" as ToggleModePreference,\n defaultOn: true,\n label: \"Live Sync\",\n speed: 1,\n size: 1,\n opacity: 1,\n hue: 0,\n saturation: 1,\n brightness: 1,\n} as const;\n\nexport function GlassToggle({\n mode = GLASS_TOGGLE_DEFAULTS.mode,\n defaultOn = GLASS_TOGGLE_DEFAULTS.defaultOn,\n label = GLASS_TOGGLE_DEFAULTS.label,\n speed = GLASS_TOGGLE_DEFAULTS.speed,\n size = GLASS_TOGGLE_DEFAULTS.size,\n opacity = GLASS_TOGGLE_DEFAULTS.opacity,\n hue = GLASS_TOGGLE_DEFAULTS.hue,\n saturation = GLASS_TOGGLE_DEFAULTS.saturation,\n brightness = GLASS_TOGGLE_DEFAULTS.brightness,\n onChange,\n className,\n style,\n}: GlassToggleProps) {\n const resolvedMode = useToggleMode(mode);\n const hostRef = useRef(null);\n const canvasRef = useRef(null);\n const switchRef = useRef(null);\n const sceneRef = useRef(null);\n const [on, setOn] = useState(defaultOn);\n const [supported, setSupported] = useState(true);\n\n const flip = useCallback(() => {\n setOn((current) => {\n const next = !current;\n onChange?.(next);\n return next;\n });\n }, [onChange]);\n\n useEffect(() => {\n const host = hostRef.current;\n const canvas = canvasRef.current;\n if (!host || !canvas) return undefined;\n\n let scene: GlassToggleScene;\n try {\n scene = createGlassToggleScene({ canvas, mode: resolvedMode, speed, on: defaultOn });\n } catch {\n setSupported(false);\n return undefined;\n }\n sceneRef.current = scene;\n\n const syncSwitchFootprint = () => {\n const control = switchRef.current;\n if (!control) return;\n const footprint = scene.measureSwitch();\n control.style.width = `${Math.round(footprint.width)}px`;\n control.style.height = `${Math.round(footprint.height)}px`;\n };\n\n const observer = new ResizeObserver((entries) => {\n const box = entries[0]?.contentRect;\n if (!box) return;\n scene.resize(box.width, box.height);\n syncSwitchFootprint();\n });\n observer.observe(host);\n scene.resize(host.clientWidth, host.clientHeight);\n syncSwitchFootprint();\n\n return () => {\n observer.disconnect();\n scene.dispose();\n sceneRef.current = null;\n };\n /* The scene is rebuilt only for the host element; appearance, speed and\n state are pushed through the imperative handles below. */\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n sceneRef.current?.setMode(resolvedMode);\n }, [resolvedMode]);\n\n useEffect(() => {\n sceneRef.current?.setSpeed(clamp(speed, 0, 3));\n }, [speed]);\n\n useEffect(() => {\n sceneRef.current?.setSize(clamp(size, 0.35, 2.5));\n const control = switchRef.current;\n const footprint = sceneRef.current?.measureSwitch();\n if (!control || !footprint) return;\n control.style.width = `${Math.round(footprint.width)}px`;\n control.style.height = `${Math.round(footprint.height)}px`;\n }, [size]);\n\n useEffect(() => {\n sceneRef.current?.setOn(on);\n }, [on]);\n\n const handlePointerMove = (event: PointerEvent) => {\n const host = hostRef.current;\n if (!host) return;\n const rect = host.getBoundingClientRect();\n const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n const y = ((event.clientY - rect.top) / rect.height) * 2 - 1;\n sceneRef.current?.setPointer(clamp(x, -1, 1), clamp(-y, -1, 1));\n };\n\n const stageStyle = {\n \"--glass-toggle-hue\": `${clamp(hue, -180, 180)}deg`,\n \"--glass-toggle-saturation\": clamp(saturation, 0, 2),\n \"--glass-toggle-brightness\": clamp(brightness, 0.35, 1.65),\n opacity: clamp(opacity, 0.05, 1),\n ...style,\n } as CSSProperties;\n\n return (\n sceneRef.current?.setPointer(0, 0)}\n onClick={flip}\n >\n \n {\n /* The whole stage is clickable, so keep the button from toggling twice. */\n event.stopPropagation();\n flip();\n }}\n />\n

\n {label}\n {on ? \"On\" : \"Off\"}\n

\n {supported ? null : (\n

This variant needs WebGL, which this browser did not provide.

\n )}\n
\n );\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/glassToggleScene.ts", + "language": "typescript", + "role": "variant-renderer", + "bytes": 23841, + "lines": 663, + "sha256": "a708e388eaec0a1feab2b97b59e523eda92c5083202bf0015d7554d9928c4895", + "code": "import * as THREE from \"three\";\n\nimport type { ToggleMode } from \"./toggleMode\";\n\nexport type GlassToggleSceneOptions = {\n canvas: HTMLCanvasElement;\n mode: ToggleMode;\n speed: number;\n on: boolean;\n};\n\nexport type GlassToggleScene = {\n setMode: (mode: ToggleMode) => void;\n setSpeed: (speed: number) => void;\n setSize: (size: number) => void;\n setOn: (on: boolean) => void;\n setPointer: (x: number, y: number) => void;\n resize: (width: number, height: number) => void;\n /* Pixel footprint of the rendered pill, so the DOM control can sit on it. */\n measureSwitch: () => { width: number; height: number };\n dispose: () => void;\n};\n\ntype Palette = {\n /* The stage is a flat sweep, so it carries no light of its own. */\n backdrop: number;\n backdropFloor: number;\n shadow: number;\n glass: number;\n attenuationOff: number;\n attenuationOn: number;\n knob: number;\n knobTransmission: number;\n knobEnv: number;\n shellEmissive: number;\n filamentOn: number;\n filamentOff: number;\n key: number;\n keyIntensity: number;\n ambient: number;\n /* Studio built into the environment map rather than the backdrop. */\n envSkyTop: number;\n envSkyBottom: number;\n envFloor: number;\n envSoftbox: number;\n envIntensity: number;\n};\n\nconst PALETTES: Record = {\n light: {\n backdrop: 0xe9ecf1,\n backdropFloor: 0xdfe3ea,\n shadow: 0.3,\n glass: 0xf4f8ff,\n attenuationOff: 0xdfe8f5,\n attenuationOn: 0x2f6bff,\n knob: 0xfdfeff,\n knobTransmission: 0.78,\n knobEnv: 1.3,\n shellEmissive: 0.02,\n filamentOn: 0x2f6bff,\n filamentOff: 0xc4ccd8,\n key: 0xffffff,\n keyIntensity: 0.18,\n ambient: 0.015,\n envSkyTop: 0xffffff,\n envSkyBottom: 0x9aa4b4,\n envFloor: 0x2a3040,\n envSoftbox: 0xffffff,\n envIntensity: 1.0,\n },\n dark: {\n backdrop: 0x0a0b0f,\n backdropFloor: 0x06070a,\n shadow: 0.55,\n glass: 0xe8eefb,\n attenuationOff: 0x7f93b8,\n attenuationOn: 0x2f6bff,\n knob: 0xf2f6ff,\n knobTransmission: 0.58,\n knobEnv: 2.3,\n shellEmissive: 0.62,\n filamentOn: 0x3b7bff,\n filamentOff: 0x161a24,\n key: 0xd8e4ff,\n keyIntensity: 0.12,\n ambient: 0.006,\n envSkyTop: 0xdde6f5,\n envSkyBottom: 0x1a2030,\n envFloor: 0x04050a,\n envSoftbox: 0xffffff,\n envIntensity: 0.9,\n },\n};\n\n/* Proportioned like a physical switch: the capsule is twice as wide as it is\n tall and the knob clears the wall by a constant seat. */\nconst TRACK_RADIUS = 0.6;\nconst TRACK_LENGTH = 1.2;\nconst THUMB_RADIUS = TRACK_RADIUS - 0.225;\nconst TRAVEL = (TRACK_LENGTH / 2) * 0.8;\nconst CONTROL_WIDTH = TRACK_LENGTH + TRACK_RADIUS * 2;\n/* The pill keeps ~13% of the stage height and never more than 18% of its\n width, so all four variants read at the same size in the picker. */\nconst VIEW_HEIGHT = (TRACK_RADIUS * 2) / 0.13;\nconst WIDTH_SHARE = 0.18;\n\nfunction srgb(hex: number) {\n return new THREE.Color(hex).convertSRGBToLinear();\n}\n\nfunction hex(value: number) {\n return `#${value.toString(16).padStart(6, \"0\")}`;\n}\n\n/* A studio, not a lamp rig — and an HDR one. A canvas environment tops out at\n 1.0, which is why LDR reflections read as dull grey wash; these softboxes run\n 12-40x above the sky, so the tone mapper blows them into the hard specular\n streaks that make a surface read as glass. Everything reflected comes from\n here, which is why the stage behind the control can stay flat. */\ntype Softbox = {\n u: number;\n v: number;\n halfWidth: number;\n halfHeight: number;\n intensity: number;\n feather: number;\n warm: number;\n};\n\nconst SOFTBOXES: readonly Softbox[] = [\n /* Broad key, high and camera-left. */\n { u: 0.3, v: 0.2, halfWidth: 0.155, halfHeight: 0.135, intensity: 19, feather: 0.86, warm: 0.04 },\n /* Narrow strip: the hard highlight that runs the length of the shoulder. */\n { u: 0.52, v: 0.115, halfWidth: 0.17, halfHeight: 0.04, intensity: 15, feather: 0.9, warm: 0 },\n /* Cool rim from behind camera-right. */\n { u: 0.86, v: 0.31, halfWidth: 0.085, halfHeight: 0.09, intensity: 10, feather: 0.92, warm: -0.06 },\n /* Backlight, directly behind the subject at u=0.25. At the silhouette the\n reflection vector points straight away from the camera, so this is the\n source that draws the clean border all the way round — and it lights the\n transmission through the body at the same time. A ring around the horizon\n instead reflects onto the equator and paints a bar across the middle. */\n { u: 0.25, v: 0.5, halfWidth: 0.1, halfHeight: 0.2, intensity: 6.5, feather: 0.8, warm: 0 },\n /* A small hard source high and camera-left: the crisp catchlight on the\n sphere, which a broad softbox alone can never give. */\n { u: 0.95, v: 0.21, halfWidth: 0.028, halfHeight: 0.032, intensity: 70, feather: 0.45, warm: 0.02 },\n /* Low bounce so the underside is not dead black. */\n { u: 0.5, v: 0.82, halfWidth: 0.5, halfHeight: 0.22, intensity: 0.5, feather: 1, warm: 0.02 },\n];\n\nfunction smoothFalloff(distance: number, extent: number, feather: number) {\n const inner = extent * (1 - feather);\n if (distance <= inner) return 1;\n if (distance >= extent) return 0;\n const t = (distance - inner) / (extent - inner);\n return 1 - t * t * (3 - 2 * t);\n}\n\nfunction buildEnvironmentTexture(palette: Palette) {\n const width = 512;\n const height = 256;\n const data = new Float32Array(width * height * 4);\n const skyTop = srgb(palette.envSkyTop);\n const skyBottom = srgb(palette.envSkyBottom);\n const floor = srgb(palette.envFloor);\n const box = srgb(palette.envSoftbox);\n\n for (let y = 0; y < height; y += 1) {\n /* three's equirect maps texture V=0 to straight *down*, so the rows have\n to be walked in reverse for `v` to mean what the softbox list says it\n means. Without this the whole studio is upside down and the subject is\n lit from the floor. */\n const v = 1 - y / (height - 1);\n /* Sky above, floor below, blended across a wide band — a hard horizon\n reflects as a seam cutting the capsule in half. */\n const ground = smoothFalloff(Math.max(0, 0.62 - v), 0.34, 1);\n const skyMix = Math.min(1, v / 0.56);\n const skyR = skyTop.r + (skyBottom.r - skyTop.r) * skyMix;\n const skyG = skyTop.g + (skyBottom.g - skyTop.g) * skyMix;\n const skyB = skyTop.b + (skyBottom.b - skyTop.b) * skyMix;\n const fade = 1 - Math.max(0, v - 0.62) * 1.1;\n const baseR = skyR + (floor.r * fade - skyR) * ground;\n const baseG = skyG + (floor.g * fade - skyG) * ground;\n const baseB = skyB + (floor.b * fade - skyB) * ground;\n\n for (let x = 0; x < width; x += 1) {\n const u = x / (width - 1);\n let r = baseR;\n let g = baseG;\n let b = baseB;\n\n for (const light of SOFTBOXES) {\n /* Azimuth wraps, so measure the short way round. */\n let du = Math.abs(u - light.u);\n if (du > 0.5) du = 1 - du;\n const dv = Math.abs(v - light.v);\n /* Elliptical, not separable: a product of two 1D falloffs reflects as a\n rectangle with visible corners. */\n const radial = Math.sqrt(\n (du / light.halfWidth) * (du / light.halfWidth)\n + (dv / light.halfHeight) * (dv / light.halfHeight),\n );\n if (radial >= 1) continue;\n const strength = smoothFalloff(radial, 1, light.feather) * light.intensity;\n r += box.r * strength * (1 + light.warm);\n g += box.g * strength;\n b += box.b * strength * (1 - light.warm);\n }\n\n const index = (y * width + x) * 4;\n data[index] = r;\n data[index + 1] = g;\n data[index + 2] = b;\n data[index + 3] = 1;\n }\n }\n\n const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.FloatType);\n texture.mapping = THREE.EquirectangularReflectionMapping;\n texture.minFilter = THREE.LinearFilter;\n texture.magFilter = THREE.LinearFilter;\n texture.generateMipmaps = false;\n texture.needsUpdate = true;\n return texture;\n}\n\n/* Micro-surface: real glass is never perfectly smooth, and the tiny variation\n is what stops a reflection from reading as a flat gradient. */\nfunction buildMicroTextures() {\n const size = 512;\n const canvas = document.createElement(\"canvas\");\n canvas.width = size;\n canvas.height = size;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n const image = ctx.createImageData(size, size);\n const height = new Float32Array(size * size);\n let seed = 0x9e3779b9;\n const random = () => {\n seed ^= seed << 13;\n seed ^= seed >>> 17;\n seed ^= seed << 5;\n return ((seed >>> 0) % 100000) / 100000;\n };\n\n /* Two octaves of blurred value noise plus a faint polishing grain. */\n const coarse = new Float32Array(64 * 64);\n for (let i = 0; i < coarse.length; i += 1) coarse[i] = random();\n for (let y = 0; y < size; y += 1) {\n for (let x = 0; x < size; x += 1) {\n const u = (x / size) * 64;\n const v = (y / size) * 64;\n const x0 = Math.floor(u);\n const y0 = Math.floor(v);\n const fx = u - x0;\n const fy = v - y0;\n const sx = fx * fx * (3 - 2 * fx);\n const sy = fy * fy * (3 - 2 * fy);\n const at = (ax: number, ay: number) => coarse[((ay & 63) * 64) + (ax & 63)];\n const a = at(x0, y0);\n const b = at(x0 + 1, y0);\n const c = at(x0, y0 + 1);\n const d = at(x0 + 1, y0 + 1);\n const value = (a * (1 - sx) + b * sx) * (1 - sy) + (c * (1 - sx) + d * sx) * sy;\n const grain = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;\n height[(y * size) + x] = value * 0.86 + (grain - Math.floor(grain)) * 0.14;\n }\n }\n\n for (let i = 0; i < height.length; i += 1) {\n /* Mostly smooth, with a shallow floor so roughness never reaches zero. */\n const value = 168 + height[i] * 78;\n image.data[i * 4] = value;\n image.data[i * 4 + 1] = value;\n image.data[i * 4 + 2] = value;\n image.data[i * 4 + 3] = 255;\n }\n ctx.putImageData(image, 0, 0);\n const roughness = new THREE.CanvasTexture(canvas);\n roughness.wrapS = THREE.RepeatWrapping;\n roughness.wrapT = THREE.RepeatWrapping;\n\n /* Derive a matching normal map so the micro-relief bends highlights too. */\n const normalCanvas = document.createElement(\"canvas\");\n normalCanvas.width = size;\n normalCanvas.height = size;\n const normalCtx = normalCanvas.getContext(\"2d\");\n if (!normalCtx) return { roughness, normal: null };\n const normalImage = normalCtx.createImageData(size, size);\n const at = (x: number, y: number) => height[((y + size) % size) * size + ((x + size) % size)];\n for (let y = 0; y < size; y += 1) {\n for (let x = 0; x < size; x += 1) {\n const dx = at(x + 1, y) - at(x - 1, y);\n const dy = at(x, y + 1) - at(x, y - 1);\n const index = (y * size + x) * 4;\n normalImage.data[index] = 128 + dx * 110;\n normalImage.data[index + 1] = 128 - dy * 110;\n normalImage.data[index + 2] = 255;\n normalImage.data[index + 3] = 255;\n }\n }\n normalCtx.putImageData(normalImage, 0, 0);\n const normal = new THREE.CanvasTexture(normalCanvas);\n normal.wrapS = THREE.RepeatWrapping;\n normal.wrapT = THREE.RepeatWrapping;\n return { roughness, normal };\n}\n\nconst BACKDROP_VERTEX = /* glsl */ `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\n/* Deliberately featureless: a seamless sweep with one contact shadow. All the\n detail in the render comes off the environment map. */\nconst BACKDROP_FRAGMENT = /* glsl */ `\nprecision highp float;\nvarying vec2 vUv;\nuniform float uAspect;\nuniform float uShadow;\nuniform vec3 uBackdrop;\nuniform vec3 uFloor;\n\nvoid main() {\n vec2 p = vec2((vUv.x - 0.5) * uAspect, vUv.y - 0.5);\n vec3 color = mix(uFloor, uBackdrop, smoothstep(-0.42, 0.16, p.y));\n\n /* Contact shadow, baked into the opaque sweep so the glass refracts it. */\n vec2 core = (p - vec2(0.0, -0.052)) / vec2(0.148, 0.038);\n vec2 spread = (p - vec2(0.0, -0.062)) / vec2(0.28, 0.1);\n float occlusion = exp(-dot(core, core) * 0.9) * 0.72 + exp(-dot(spread, spread) * 0.8) * 0.42;\n color *= 1.0 - clamp(occlusion, 0.0, 1.0) * uShadow;\n\n /* A dither step wide enough to kill banding on a near-flat sweep. */\n float dither = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);\n color += (dither - 0.5) * 0.0035;\n\n gl_FragColor = vec4(color, 1.0);\n}\n`;\n\nexport function createGlassToggleScene({\n canvas,\n mode,\n speed,\n on,\n}: GlassToggleSceneOptions): GlassToggleScene {\n const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n renderer.outputEncoding = THREE.sRGBEncoding;\n renderer.toneMapping = THREE.ACESFilmicToneMapping;\n renderer.toneMappingExposure = 1;\n\n const scene = new THREE.Scene();\n const camera = new THREE.PerspectiveCamera(26, 1, 0.1, 40);\n camera.position.set(0, 0.22, 9.4);\n camera.lookAt(0, 0, 0);\n\n const pmrem = new THREE.PMREMGenerator(renderer);\n pmrem.compileEquirectangularShader();\n\n const backdropUniforms = {\n uAspect: { value: 1 },\n uShadow: { value: PALETTES[mode].shadow },\n uBackdrop: { value: srgb(PALETTES[mode].backdrop) },\n uFloor: { value: srgb(PALETTES[mode].backdropFloor) },\n };\n\n const backdrop = new THREE.Mesh(\n new THREE.PlaneGeometry(1, 1),\n new THREE.ShaderMaterial({\n uniforms: backdropUniforms,\n vertexShader: BACKDROP_VERTEX,\n fragmentShader: BACKDROP_FRAGMENT,\n depthWrite: false,\n }),\n );\n backdrop.position.z = -3.2;\n backdrop.renderOrder = -1;\n scene.add(backdrop);\n\n const micro = buildMicroTextures();\n const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();\n if (micro?.roughness) {\n micro.roughness.anisotropy = maxAnisotropy;\n micro.roughness.repeat.set(3, 2);\n }\n if (micro?.normal) {\n micro.normal.anisotropy = maxAnisotropy;\n micro.normal.repeat.set(3, 2);\n }\n\n /* One rig, turned a few degrees so the capsule reads as a solid object. */\n const rig = new THREE.Group();\n rig.rotation.set(-0.05, 0.13, 0);\n scene.add(rig);\n\n const glassMaterial = new THREE.MeshPhysicalMaterial({\n color: srgb(PALETTES[mode].glass),\n metalness: 0,\n roughness: 0.115,\n transmission: 1,\n ior: 1.62,\n clearcoat: 1,\n clearcoatRoughness: 0.008,\n attenuationColor: srgb(PALETTES[mode].attenuationOff),\n attenuationDistance: 6,\n envMapIntensity: PALETTES[mode].envIntensity,\n specularIntensity: 1,\n roughnessMap: micro?.roughness ?? null,\n clearcoatNormalMap: micro?.normal ?? null,\n clearcoatNormalScale: new THREE.Vector2(0.13, 0.13),\n });\n /* @types/three 0.149 leaves these out of the parameters interface. */\n glassMaterial.thickness = 1.25;\n /* A whisper of thin-film so the shoulders pick up colour the way real\n coated glass does. */\n glassMaterial.iridescence = 0.2;\n glassMaterial.iridescenceIOR = 1.3;\n glassMaterial.iridescenceThicknessRange = [120, 420];\n glassMaterial.emissive = srgb(PALETTES[mode].filamentOn);\n glassMaterial.emissiveIntensity = 0;\n\n const track = new THREE.Mesh(\n new THREE.CapsuleGeometry(TRACK_RADIUS, TRACK_LENGTH, 64, 192),\n glassMaterial,\n );\n track.rotation.z = Math.PI / 2;\n /* Flattened in depth: a switch is a lozenge, not a capsule pill. */\n track.scale.set(1, 1, 0.82);\n rig.add(track);\n\n /* A slim lit filament lying in the bottom of the trough. It stays opaque so\n it reaches the transmission backdrop and the shell actually refracts it. */\n const filamentMaterial = new THREE.MeshStandardMaterial({\n color: srgb(PALETTES[mode].filamentOff),\n emissive: srgb(PALETTES[mode].filamentOn),\n emissiveIntensity: on ? 1 : 0,\n roughness: 0.34,\n metalness: 0,\n });\n const filament = new THREE.Mesh(\n new THREE.CapsuleGeometry(0.105, TRACK_LENGTH + 0.16, 16, 64),\n filamentMaterial,\n );\n filament.rotation.z = Math.PI / 2;\n filament.position.set(0, -0.31, -0.04);\n rig.add(filament);\n\n /* The knob is a frosted lens standing proud of the capsule, so it refracts\n both the sweep behind the stage and the filament inside the trough. */\n const knobMaterial = new THREE.MeshPhysicalMaterial({\n color: srgb(PALETTES[mode].knob),\n metalness: 0,\n roughness: 0.125,\n transmission: PALETTES[mode].knobTransmission,\n ior: 1.47,\n clearcoat: 1,\n clearcoatRoughness: 0.01,\n attenuationColor: srgb(PALETTES[mode].attenuationOff),\n attenuationDistance: 3,\n envMapIntensity: PALETTES[mode].envIntensity * PALETTES[mode].knobEnv,\n roughnessMap: micro?.roughness ?? null,\n clearcoatNormalMap: micro?.normal ?? null,\n clearcoatNormalScale: new THREE.Vector2(0.1, 0.1),\n });\n knobMaterial.thickness = 0.62;\n knobMaterial.iridescence = 0.12;\n knobMaterial.iridescenceIOR = 1.25;\n knobMaterial.iridescenceThicknessRange = [100, 380];\n\n const knob = new THREE.Mesh(new THREE.SphereGeometry(THUMB_RADIUS, 160, 96), knobMaterial);\n /* Left perfectly spherical — no depth squash. Its equator has to sit at the\n shell's front surface (TRACK_RADIUS * the depth flattening): any deeper and\n the silhouette is read through the curved wall, which refracts it into an\n egg. At the surface the outline is a true circle and the back half still\n sits down in the trough. */\n knob.position.z = TRACK_RADIUS * 0.82;\n rig.add(knob);\n\n /* IBL does nearly all the work; the key is only here to keep a terminator. */\n const ambient = new THREE.AmbientLight(srgb(0xffffff), PALETTES[mode].ambient);\n scene.add(ambient);\n\n const key = new THREE.DirectionalLight(srgb(PALETTES[mode].key), PALETTES[mode].keyIntensity);\n key.position.set(2.2, 3.4, 4);\n scene.add(key);\n\n let environmentTarget: THREE.WebGLRenderTarget | null = null;\n\n function applyEnvironment(next: ToggleMode) {\n const source = buildEnvironmentTexture(PALETTES[next]);\n if (!source) return;\n const nextTarget = pmrem.fromEquirectangular(source);\n source.dispose();\n environmentTarget?.dispose();\n environmentTarget = nextTarget;\n scene.environment = nextTarget.texture;\n }\n\n applyEnvironment(mode);\n\n let currentMode = mode;\n let currentSpeed = speed;\n let target = on ? 1 : 0;\n let progress = target;\n let velocity = 0;\n let onGlow = target;\n const attenuationOffColor = srgb(PALETTES[mode].attenuationOff);\n const attenuationOnColor = srgb(PALETTES[mode].attenuationOn);\n const filamentOffColor = srgb(PALETTES[mode].filamentOff);\n const filamentOnColor = srgb(PALETTES[mode].filamentOn);\n const pointer = new THREE.Vector2(0, 0);\n const smoothedPointer = new THREE.Vector2(0, 0);\n let width = 1;\n let height = 1;\n let baseDistance = camera.position.z;\n let sizeScale = 1;\n let disposed = false;\n let last = performance.now();\n let elapsed = 0;\n\n const reduceMotion = typeof window !== \"undefined\"\n && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n function applyPalette(next: ToggleMode) {\n const palette = PALETTES[next];\n backdropUniforms.uBackdrop.value = srgb(palette.backdrop);\n backdropUniforms.uFloor.value = srgb(palette.backdropFloor);\n backdropUniforms.uShadow.value = palette.shadow;\n glassMaterial.color = srgb(palette.glass);\n glassMaterial.envMapIntensity = palette.envIntensity;\n knobMaterial.color = srgb(palette.knob);\n knobMaterial.attenuationColor = srgb(palette.attenuationOff);\n knobMaterial.transmission = palette.knobTransmission;\n knobMaterial.envMapIntensity = palette.envIntensity * palette.knobEnv;\n attenuationOffColor.copy(srgb(palette.attenuationOff));\n attenuationOnColor.copy(srgb(palette.attenuationOn));\n filamentOffColor.copy(srgb(palette.filamentOff));\n filamentOnColor.copy(srgb(palette.filamentOn));\n filamentMaterial.emissive = srgb(palette.filamentOn);\n glassMaterial.emissive = srgb(palette.filamentOn);\n ambient.intensity = palette.ambient;\n key.color = srgb(palette.key);\n key.intensity = palette.keyIntensity;\n applyEnvironment(next);\n }\n\n function resize(nextWidth: number, nextHeight: number) {\n width = Math.max(1, Math.round(nextWidth));\n height = Math.max(1, Math.round(nextHeight));\n renderer.setSize(width, height, false);\n camera.aspect = width / height;\n\n /* Frame the control identically at every stage size by moving the camera\n rather than scaling the meshes — glass thickness is in world units, so a\n scaled pill would refract differently at every viewport. */\n const halfFov = (camera.fov * Math.PI) / 360;\n const visibleHeight = Math.max(VIEW_HEIGHT, CONTROL_WIDTH / (WIDTH_SHARE * camera.aspect)) / sizeScale;\n baseDistance = visibleHeight / (2 * Math.tan(halfFov));\n camera.position.z = baseDistance;\n camera.updateProjectionMatrix();\n\n const backdropHeight = 2 * (baseDistance - backdrop.position.z) * Math.tan(halfFov);\n backdrop.scale.set(backdropHeight * camera.aspect * 1.08, backdropHeight * 1.08, 1);\n backdropUniforms.uAspect.value = camera.aspect;\n }\n\n function measureSwitch() {\n const halfWidth = TRACK_LENGTH / 2 + TRACK_RADIUS;\n const left = new THREE.Vector3(-halfWidth, 0, 0).project(camera);\n const right = new THREE.Vector3(halfWidth, 0, 0).project(camera);\n const top = new THREE.Vector3(0, TRACK_RADIUS, 0).project(camera);\n const bottom = new THREE.Vector3(0, -TRACK_RADIUS, 0).project(camera);\n return {\n width: Math.abs(right.x - left.x) * 0.5 * width,\n height: Math.abs(top.y - bottom.y) * 0.5 * height,\n };\n }\n\n function frame(now: number) {\n if (disposed) return;\n const delta = Math.min(0.05, (now - last) / 1000);\n last = now;\n elapsed += delta * currentSpeed;\n\n /* Critically damped travel — it arrives without the rubbery overshoot a\n spring would give a glass object this heavy-looking. */\n const stiffness = reduceMotion ? 400 : 150;\n const damping = 2 * Math.sqrt(stiffness);\n const step = reduceMotion ? Math.min(delta, 0.016) : delta;\n velocity += (target - progress) * stiffness * step - velocity * damping * step;\n progress += velocity * step;\n\n onGlow += (target - onGlow) * Math.min(1, delta * 6);\n smoothedPointer.lerp(pointer, Math.min(1, delta * 3.2));\n\n knob.position.x = (progress * 2 - 1) * TRAVEL;\n knob.rotation.z = -progress * Math.PI * 0.9;\n knob.rotation.y = smoothedPointer.x * 0.3;\n\n filamentMaterial.emissiveIntensity = onGlow * (currentMode === \"dark\" ? 3.2 : 1.4);\n filamentMaterial.color.copy(filamentOffColor).lerp(filamentOnColor, onGlow * 0.85);\n glassMaterial.attenuationColor.copy(attenuationOffColor).lerp(attenuationOnColor, onGlow);\n glassMaterial.attenuationDistance = 6 - onGlow * 4.8;\n glassMaterial.emissiveIntensity = onGlow * PALETTES[currentMode].shellEmissive;\n\n camera.position.set(\n smoothedPointer.x * 0.5,\n 0.22 + smoothedPointer.y * 0.32,\n baseDistance,\n );\n camera.lookAt(0, 0, 0);\n\n renderer.render(scene, camera);\n window.requestAnimationFrame(frame);\n }\n\n window.requestAnimationFrame(frame);\n\n return {\n setMode(next) {\n if (next === currentMode) return;\n currentMode = next;\n applyPalette(next);\n },\n setSpeed(next) {\n currentSpeed = next;\n },\n setSize(next) {\n sizeScale = next;\n resize(width, height);\n },\n setOn(next) {\n target = next ? 1 : 0;\n },\n setPointer(x, y) {\n pointer.set(x, y);\n },\n resize,\n measureSwitch,\n dispose() {\n disposed = true;\n environmentTarget?.dispose();\n pmrem.dispose();\n micro?.roughness?.dispose();\n micro?.normal?.dispose();\n scene.traverse((object) => {\n if (!(object instanceof THREE.Mesh)) return;\n object.geometry.dispose();\n const material = object.material;\n if (Array.isArray(material)) material.forEach((entry) => entry.dispose());\n else material.dispose();\n });\n renderer.dispose();\n },\n };\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/glass-toggle.css", + "language": "css", + "role": "variant-style", + "bytes": 2718, + "lines": 109, + "sha256": "d87d193e981e191dbab386f1dc7dbcfbe46fb709bcc7085305455acd217765f3", + "code": ".glass-toggle,\n.glass-toggle *,\n.glass-toggle *::before,\n.glass-toggle *::after {\n box-sizing: border-box;\n}\n\n.glass-toggle {\n --glass-toggle-hue: 0deg;\n --glass-toggle-saturation: 1;\n --glass-toggle-brightness: 1;\n --glass-toggle-stage: #eef1f6;\n --glass-toggle-caption: rgba(17, 22, 34, 0.5);\n --glass-toggle-caption-on: rgba(17, 22, 34, 0.78);\n --glass-toggle-focus: rgba(58, 120, 255, 0.45);\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n isolation: isolate;\n container-type: size;\n background: var(--glass-toggle-stage);\n color-scheme: light;\n filter: hue-rotate(var(--glass-toggle-hue)) saturate(var(--glass-toggle-saturation)) brightness(var(--glass-toggle-brightness));\n font-family: ui-sans-serif, -apple-system, \"SF Pro Text\", \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n}\n\n.glass-toggle[data-mode=\"dark\"] {\n --glass-toggle-stage: #08090d;\n --glass-toggle-caption: rgba(233, 238, 250, 0.46);\n --glass-toggle-caption-on: rgba(233, 238, 250, 0.82);\n --glass-toggle-focus: rgba(122, 168, 255, 0.5);\n color-scheme: dark;\n}\n\n.glass-toggle__canvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n/* A real control sitting exactly over the rendered pill, so the scene stays\n keyboard- and screen-reader-operable. */\n.glass-toggle__switch {\n position: absolute;\n left: 50%;\n top: 50%;\n width: 200px;\n height: 112px;\n padding: 0;\n border: 0;\n border-radius: 999px;\n background: transparent;\n transform: translate(-50%, -50%);\n cursor: pointer;\n appearance: none;\n}\n\n.glass-toggle__switch:focus-visible {\n outline: none;\n box-shadow: 0 0 0 4px var(--glass-toggle-focus);\n}\n\n.glass-toggle__caption {\n position: absolute;\n left: 50%;\n /* The control keeps 13% of the stage height, so the caption tracks the centre\n rather than the floor — otherwise it drifts away in a tall frame. */\n top: calc(50% + max(52px, 11cqh));\n transform: translateX(-50%);\n display: flex;\n align-items: baseline;\n gap: 0.55em;\n margin: 0;\n font-size: clamp(11px, 1.9cqw, 14px);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--glass-toggle-caption);\n transition: color 0.45s ease;\n pointer-events: none;\n}\n\n.glass-toggle[data-state=\"on\"] .glass-toggle__caption {\n color: var(--glass-toggle-caption-on);\n}\n\n.glass-toggle__caption b {\n font-weight: 500;\n}\n\n.glass-toggle__fallback {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n padding: 24px;\n text-align: center;\n font-size: 13px;\n line-height: 1.5;\n color: var(--glass-toggle-caption);\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/ShaderToggle.tsx", + "language": "tsx", + "role": "variant-component", + "bytes": 5253, + "lines": 180, + "sha256": "69bfab67ebe67b1e886c66c69451a64db9241267d922bfe774dc491df1f592ef", + "code": "import { useCallback, useEffect, useRef, useState, type CSSProperties, type PointerEvent } from \"react\";\n\nimport \"./shader-toggle.css\";\nimport { createShaderToggleScene, type ShaderToggleScene } from \"./shaderToggleScene\";\nimport { clamp, useToggleMode, type ToggleModePreference } from \"./toggleMode\";\n\nexport type ShaderToggleProps = {\n mode?: ToggleModePreference;\n defaultOn?: boolean;\n label?: string;\n speed?: number;\n size?: number;\n opacity?: number;\n hue?: number;\n saturation?: number;\n brightness?: number;\n onChange?: (on: boolean) => void;\n className?: string;\n style?: CSSProperties;\n};\n\nexport const SHADER_TOGGLE_DEFAULTS = {\n mode: \"auto\" as ToggleModePreference,\n defaultOn: true,\n label: \"Live Sync\",\n speed: 1,\n size: 1,\n opacity: 1,\n hue: 0,\n saturation: 1,\n brightness: 1,\n} as const;\n\nexport function ShaderToggle({\n mode = SHADER_TOGGLE_DEFAULTS.mode,\n defaultOn = SHADER_TOGGLE_DEFAULTS.defaultOn,\n label = SHADER_TOGGLE_DEFAULTS.label,\n speed = SHADER_TOGGLE_DEFAULTS.speed,\n size = SHADER_TOGGLE_DEFAULTS.size,\n opacity = SHADER_TOGGLE_DEFAULTS.opacity,\n hue = SHADER_TOGGLE_DEFAULTS.hue,\n saturation = SHADER_TOGGLE_DEFAULTS.saturation,\n brightness = SHADER_TOGGLE_DEFAULTS.brightness,\n onChange,\n className,\n style,\n}: ShaderToggleProps) {\n const resolvedMode = useToggleMode(mode);\n const hostRef = useRef(null);\n const canvasRef = useRef(null);\n const switchRef = useRef(null);\n const sceneRef = useRef(null);\n const [on, setOn] = useState(defaultOn);\n const [supported, setSupported] = useState(true);\n\n const flip = useCallback(() => {\n setOn((current) => {\n const next = !current;\n onChange?.(next);\n return next;\n });\n }, [onChange]);\n\n const syncSwitchFootprint = useCallback(() => {\n const control = switchRef.current;\n const footprint = sceneRef.current?.measureSwitch();\n if (!control || !footprint) return;\n control.style.width = `${Math.round(footprint.width)}px`;\n control.style.height = `${Math.round(footprint.height)}px`;\n }, []);\n\n useEffect(() => {\n const host = hostRef.current;\n const canvas = canvasRef.current;\n if (!host || !canvas) return undefined;\n\n let scene: ShaderToggleScene;\n try {\n scene = createShaderToggleScene({\n canvas,\n mode: resolvedMode,\n speed,\n size: clamp(size, 0.35, 2.5),\n on: defaultOn,\n });\n } catch {\n setSupported(false);\n return undefined;\n }\n sceneRef.current = scene;\n\n const observer = new ResizeObserver((entries) => {\n const box = entries[0]?.contentRect;\n if (!box) return;\n scene.resize(box.width, box.height);\n syncSwitchFootprint();\n });\n observer.observe(host);\n scene.resize(host.clientWidth, host.clientHeight);\n syncSwitchFootprint();\n\n return () => {\n observer.disconnect();\n scene.dispose();\n sceneRef.current = null;\n };\n /* Built once for the host element; every knob below is pushed in. */\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n useEffect(() => {\n sceneRef.current?.setMode(resolvedMode);\n }, [resolvedMode]);\n\n useEffect(() => {\n sceneRef.current?.setSpeed(clamp(speed, 0, 3));\n }, [speed]);\n\n useEffect(() => {\n sceneRef.current?.setSize(clamp(size, 0.35, 2.5));\n syncSwitchFootprint();\n }, [size, syncSwitchFootprint]);\n\n useEffect(() => {\n sceneRef.current?.setOn(on);\n }, [on]);\n\n const handlePointerMove = (event: PointerEvent) => {\n const host = hostRef.current;\n if (!host) return;\n const rect = host.getBoundingClientRect();\n const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n const y = ((event.clientY - rect.top) / rect.height) * 2 - 1;\n sceneRef.current?.setPointer(clamp(x, -1, 1), clamp(-y, -1, 1));\n };\n\n const stageStyle = {\n \"--shader-toggle-hue\": `${clamp(hue, -180, 180)}deg`,\n \"--shader-toggle-saturation\": clamp(saturation, 0, 2),\n \"--shader-toggle-brightness\": clamp(brightness, 0.35, 1.65),\n opacity: clamp(opacity, 0.05, 1),\n ...style,\n } as CSSProperties;\n\n return (\n sceneRef.current?.setPointer(0, 0)}\n onClick={flip}\n >\n \n {\n /* The whole stage is clickable, so keep the button from toggling twice. */\n event.stopPropagation();\n flip();\n }}\n />\n

\n {label}\n {on ? \"On\" : \"Off\"}\n

\n {supported ? null : (\n

This variant needs WebGL, which this browser did not provide.

\n )}\n
\n );\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/shaderToggleScene.ts", + "language": "typescript", + "role": "variant-renderer", + "bytes": 6726, + "lines": 200, + "sha256": "6eadd83c7abeaadb2bb4a9c44624234050f7faf6a36af6ace8738b36f9dfb673", + "code": "import { buildShaderToggleFragment, SHADER_TOGGLE_VERTEX } from \"./shaderToggleGlsl\";\nimport type { ToggleMode } from \"./toggleMode\";\n\nexport type ShaderToggleSceneOptions = {\n canvas: HTMLCanvasElement;\n mode: ToggleMode;\n speed: number;\n size: number;\n on: boolean;\n};\n\nexport type ShaderToggleScene = {\n setMode: (mode: ToggleMode) => void;\n setSpeed: (speed: number) => void;\n setSize: (size: number) => void;\n setOn: (on: boolean) => void;\n setPointer: (x: number, y: number) => void;\n resize: (width: number, height: number) => void;\n /* CSS-pixel footprint of the drawn capsule, so the DOM control matches it. */\n measureSwitch: () => { width: number; height: number };\n dispose: () => void;\n};\n\n/* Control-space constants, shared with the fragment shader. */\nconst TRACK_HALF_LENGTH = 1.35;\nconst TRACK_RADIUS = 1;\nconst HEIGHT_SHARE = 0.13;\nconst WIDTH_SHARE = 0.18;\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string) {\n const shader = gl.createShader(type);\n if (!shader) throw new Error(\"Shader could not be created.\");\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader);\n gl.deleteShader(shader);\n throw new Error(`Shader failed to compile: ${log ?? \"unknown error\"}`);\n }\n return shader;\n}\n\nexport function createShaderToggleScene({\n canvas,\n mode,\n speed,\n size,\n on,\n}: ShaderToggleSceneOptions): ShaderToggleScene {\n const context = canvas.getContext(\"webgl\", { antialias: true, alpha: false })\n ?? canvas.getContext(\"experimental-webgl\", { antialias: true, alpha: false });\n if (!context) throw new Error(\"WebGL is unavailable.\");\n const gl = context as WebGLRenderingContext;\n\n /* Derivatives give the distance fields a one-pixel edge at any scale; the\n shader falls back to a fixed width when the extension is absent. */\n const hasDerivatives = Boolean(gl.getExtension(\"OES_standard_derivatives\"));\n\n const program = gl.createProgram();\n if (!program) throw new Error(\"Program could not be created.\");\n const vertex = compile(gl, gl.VERTEX_SHADER, SHADER_TOGGLE_VERTEX);\n const fragment = compile(gl, gl.FRAGMENT_SHADER, buildShaderToggleFragment(hasDerivatives));\n gl.attachShader(program, vertex);\n gl.attachShader(program, fragment);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(program);\n throw new Error(`Program failed to link: ${log ?? \"unknown error\"}`);\n }\n gl.deleteShader(vertex);\n gl.deleteShader(fragment);\n gl.useProgram(program);\n\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n const position = gl.getAttribLocation(program, \"aPosition\");\n gl.enableVertexAttribArray(position);\n gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n\n const uniforms = {\n res: gl.getUniformLocation(program, \"uRes\"),\n unit: gl.getUniformLocation(program, \"uUnit\"),\n time: gl.getUniformLocation(program, \"uTime\"),\n on: gl.getUniformLocation(program, \"uOn\"),\n progress: gl.getUniformLocation(program, \"uProgress\"),\n mode: gl.getUniformLocation(program, \"uMode\"),\n pointer: gl.getUniformLocation(program, \"uPointer\"),\n };\n\n const pixelRatio = Math.min(typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1, 2);\n const reduceMotion = typeof window !== \"undefined\"\n && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n let width = 1;\n let height = 1;\n let unit = 1;\n let sizeScale = size;\n let currentMode = mode;\n let currentSpeed = speed;\n let target = on ? 1 : 0;\n let progress = target;\n let velocity = 0;\n let glow = target;\n let pointerX = 0;\n let pointerY = 0;\n let smoothX = 0;\n let smoothY = 0;\n let elapsed = 0;\n let last = typeof performance === \"undefined\" ? 0 : performance.now();\n let disposed = false;\n let handle = 0;\n\n function layout() {\n /* The capsule keeps ~13% of the stage height and never spills past 18% of\n its width, so all four variants read at the same size in the picker. */\n unit = Math.min(\n (height * HEIGHT_SHARE) / (TRACK_RADIUS * 2),\n (width * WIDTH_SHARE) / ((TRACK_HALF_LENGTH + TRACK_RADIUS) * 2),\n ) * sizeScale;\n }\n\n function resize(nextWidth: number, nextHeight: number) {\n width = Math.max(1, Math.round(nextWidth));\n height = Math.max(1, Math.round(nextHeight));\n canvas.width = Math.round(width * pixelRatio);\n canvas.height = Math.round(height * pixelRatio);\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n gl.viewport(0, 0, canvas.width, canvas.height);\n layout();\n }\n\n function measureSwitch() {\n return {\n width: unit * (TRACK_HALF_LENGTH + TRACK_RADIUS) * 2,\n height: unit * TRACK_RADIUS * 2,\n };\n }\n\n function frame(now: number) {\n if (disposed) return;\n const delta = Math.min(0.05, (now - last) / 1000);\n last = now;\n elapsed += delta * currentSpeed;\n\n const stiffness = reduceMotion ? 420 : 165;\n const damping = 2 * Math.sqrt(stiffness);\n const step = reduceMotion ? Math.min(delta, 0.016) : delta;\n velocity += (target - progress) * stiffness * step - velocity * damping * step;\n progress += velocity * step;\n glow += (target - glow) * Math.min(1, delta * 5.5);\n smoothX += (pointerX - smoothX) * Math.min(1, delta * 3.4);\n smoothY += (pointerY - smoothY) * Math.min(1, delta * 3.4);\n\n gl.uniform2f(uniforms.res, canvas.width, canvas.height);\n gl.uniform1f(uniforms.unit, unit * pixelRatio);\n gl.uniform1f(uniforms.time, elapsed);\n gl.uniform1f(uniforms.on, glow);\n gl.uniform1f(uniforms.progress, progress);\n gl.uniform1f(uniforms.mode, currentMode === \"dark\" ? 1 : 0);\n gl.uniform2f(uniforms.pointer, smoothX, smoothY);\n gl.drawArrays(gl.TRIANGLES, 0, 3);\n\n handle = window.requestAnimationFrame(frame);\n }\n\n handle = window.requestAnimationFrame(frame);\n\n return {\n setMode(next) {\n currentMode = next;\n },\n setSpeed(next) {\n currentSpeed = next;\n },\n setSize(next) {\n sizeScale = next;\n layout();\n },\n setOn(next) {\n target = next ? 1 : 0;\n },\n setPointer(x, y) {\n pointerX = x;\n pointerY = y;\n },\n resize,\n measureSwitch,\n dispose() {\n disposed = true;\n window.cancelAnimationFrame(handle);\n gl.deleteBuffer(buffer);\n gl.deleteProgram(program);\n /* Never force a context loss here: React remounts this effect on the same\n canvas in StrictMode, and a lost context can never be re-acquired. */\n },\n };\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/shaderToggleGlsl.ts", + "language": "typescript", + "role": "shader-source", + "bytes": 12519, + "lines": 299, + "sha256": "12eef096246b9fe460ab256e0369b846266a3e87fe5869760dc4e67c9bfa0b57", + "code": "export const SHADER_TOGGLE_VERTEX = `\nattribute vec2 aPosition;\nvoid main() {\n gl_Position = vec4(aPosition, 0.0, 1.0);\n}\n`;\n\n/* One fragment shader draws the whole control. Every edge is antialiased from\n the screen-space derivative of its own distance field rather than a fixed\n smoothstep, so the borders stay one pixel crisp at any size — a constant\n width blurs the rim as soon as the control is scaled up. */\nconst SHADER_TOGGLE_FRAGMENT_BODY = `\nprecision highp float;\n\nuniform vec2 uRes;\nuniform float uUnit;\nuniform float uTime;\nuniform float uOn;\nuniform float uProgress;\nuniform float uMode;\nuniform vec2 uPointer;\n\nconst float R = 1.0;\nconst float L = 1.35;\nconst float TH = 0.78;\nconst float PI = 3.14159265359;\n\nfloat hash11(float n) {\n return fract(sin(n * 127.1) * 43758.5453123);\n}\n\nvec2 hash21(float n) {\n return fract(sin(vec2(n * 127.1, n * 311.7)) * 43758.5453123);\n}\n\n/* Screen-space hash. Feeding raw gl_FragCoord into a sin-based hash runs out of\n float precision at 1080p and bands into visible diagonal wedges, so wrap the\n coordinate into a small range first. */\nfloat hash22(vec2 co) {\n return fract(sin(dot(mod(co, 512.0), vec2(12.9898, 78.233))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = hash11(i.x + i.y * 57.0);\n float b = hash11(i.x + 1.0 + i.y * 57.0);\n float c = hash11(i.x + (i.y + 1.0) * 57.0);\n float d = hash11(i.x + 1.0 + (i.y + 1.0) * 57.0);\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < 5; i++) {\n value += amplitude * valueNoise(p);\n p = p * 2.03 + 11.3;\n amplitude *= 0.5;\n }\n return value;\n}\n\n/* Ridged noise reads as filament rather than cloud — it is what gives the\n trough its threaded, fibrous look instead of a soft blur. */\nfloat ridged(vec2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < 4; i++) {\n float n = 1.0 - abs(valueNoise(p) * 2.0 - 1.0);\n value += amplitude * n * n;\n p = p * 2.11 + 7.7;\n amplitude *= 0.5;\n }\n return value;\n}\n\n/* Two rounds of domain warping. One fbm on its own drifts; warping it by\n another turns the flow into curling filaments that read as real motion. */\nvec2 warp(vec2 p, float t) {\n vec2 q = vec2(fbm(p + vec2(0.0, t * 0.12)), fbm(p + vec2(5.2, 1.3) - t * 0.09));\n vec2 r = vec2(fbm(p + 3.4 * q + vec2(1.7, 9.2)), fbm(p + 3.4 * q + vec2(8.3, 2.8)));\n return p + 0.42 * r;\n}\n\nfloat sdCapsule(vec2 p, float halfLength, float radius) {\n p.x -= clamp(p.x, -halfLength, halfLength);\n return length(p) - radius;\n}\n\nfloat fw(float d) {\n#ifdef HAS_DERIVATIVES\n return max(fwidth(d), 1e-5);\n#else\n return 1.6 / uUnit;\n#endif\n}\n\n/* Filled side of a distance field, one pixel of coverage at the boundary. */\nfloat fill(float d) {\n float w = fw(d) * 0.72;\n return smoothstep(w, -w, d);\n}\n\n/* A stroke centred on the isoline, measured in control-space units. */\nfloat stroke(float d, float halfWidth) {\n float w = fw(d) * 0.8;\n return smoothstep(halfWidth + w, halfWidth - w, abs(d));\n}\n\nfloat capsuleHalo(vec2 p, float spread) {\n return exp(-max(0.0, sdCapsule(p, L, R)) * spread);\n}\n\nvoid main() {\n vec2 frag = gl_FragCoord.xy - 0.5 * uRes;\n vec2 p = frag / uUnit;\n vec2 screen = frag / max(uRes.x, uRes.y);\n\n float dark = uMode;\n float energy = mix(0.16, 1.0, uOn);\n\n vec3 accent = mix(vec3(0.09, 0.32, 0.95), vec3(0.24, 0.55, 1.0), dark);\n vec3 accentWarm = mix(vec3(0.55, 0.18, 0.86), vec3(0.62, 0.32, 1.0), dark);\n vec3 accentCool = mix(vec3(0.06, 0.62, 0.92), vec3(0.36, 0.86, 1.0), dark);\n vec3 sparkColor = mix(vec3(0.15, 0.62, 0.98), vec3(0.55, 0.86, 1.0), dark);\n vec3 dormant = mix(vec3(0.66, 0.69, 0.75), vec3(0.16, 0.19, 0.25), dark);\n\n /* Stage. */\n vec3 skyTop = mix(vec3(0.965, 0.972, 0.984), vec3(0.055, 0.062, 0.086), dark);\n vec3 skyBottom = mix(vec3(0.886, 0.906, 0.941), vec3(0.016, 0.019, 0.031), dark);\n vec3 color = mix(skyBottom, skyTop, smoothstep(-0.55, 0.55, screen.y));\n\n /* A faint dot lattice keeps the empty stage from reading as flat paint. */\n vec2 lattice = fract(p * 1.8) - 0.5;\n float dots = smoothstep(0.09, 0.02, length(lattice));\n color += dots * mix(-0.05, 0.05, dark) * mix(vec3(0.32, 0.4, 0.62), accent, dark);\n\n /* Bloom, one exponential per channel for a touch of chromatic spread. */\n float haloR = capsuleHalo(p * 0.985, 1.35);\n float haloG = capsuleHalo(p, 1.5);\n float haloB = capsuleHalo(p * 1.015, 1.65);\n vec3 halo = vec3(haloR, haloG, haloB) * mix(accent, accentWarm, 0.35);\n color += halo * energy * mix(0.045, 0.3, dark);\n\n float d = sdCapsule(p, L, R);\n float inside = fill(d);\n\n /* ---- Trough interior ---------------------------------------------- */\n vec2 flowP = vec2(p.x * 0.72 - uTime * 0.3, p.y * 1.25);\n vec2 w = warp(flowP, uTime);\n float plasma = fbm(w * 1.5);\n float threads = ridged(vec2(w.x * 2.2, w.y * 3.4 + uTime * 0.16));\n\n /* Brushed striations along the travel axis. */\n float brushed = 0.5 + 0.5 * sin(p.y * 74.0 + fbm(w * 3.0) * 9.0);\n brushed *= 0.5 + 0.5 * valueNoise(vec2(p.x * 26.0, p.y * 4.0));\n\n /* Interference bands, following the warp so they curve with the flow. */\n float caustic = pow(abs(sin(w.x * 3.6 - uTime * 1.5 + threads * 2.4)), 9.0);\n\n /* A micro lattice the light catches, crisp because it is derivative-AA'd. */\n vec2 cell = fract(p * vec2(22.0, 19.0)) - 0.5;\n float micro = stroke(length(cell) - 0.2, 0.035);\n\n /* A travelling charge front. */\n float streak = pow(max(0.0, sin(p.x * 2.1 - uTime * 2.6 + plasma * 3.4)), 7.0);\n\n vec3 interior = mix(dormant, mix(accent, accentWarm, plasma * 0.42), uOn);\n interior *= mix(0.24, 0.86, plasma) * energy;\n interior += mix(accentCool, vec3(1.0), 0.25) * threads * threads * uOn * 0.15;\n interior += mix(accent, vec3(1.0), 0.35) * caustic * uOn * 0.2;\n interior += accentCool * micro * mix(0.018, 0.05, dark) * energy;\n interior *= 0.82 + 0.18 * brushed;\n interior += mix(accent, vec3(1.0), 0.35) * streak * uOn * 0.34;\n\n /* Powered down still has to read as a slot, not a hole — on a light stage an\n unlit trough at the on-state's exposure goes nearly black. */\n vec3 restColor = dormant * mix(0.6, 0.2, dark) * (0.72 + 0.4 * plasma);\n restColor += accentCool * micro * mix(0.02, 0.045, dark);\n restColor *= 0.88 + 0.12 * brushed;\n interior = mix(restColor, interior, uOn);\n\n /* Inner shading: the capsule is still a physical trough. */\n float trough = smoothstep(-R, R * 0.35, p.y);\n interior *= mix(0.42, 1.2, trough);\n\n color = mix(color, interior, inside);\n\n /* ---- Borders ------------------------------------------------------- */\n /* A hairline seat just outside the wall reads as the shadow the rim casts. */\n color *= 1.0 - stroke(d - 0.028, 0.026) * mix(0.16, 0.3, dark);\n /* The lit rim itself: one crisp stroke on the boundary. */\n vec3 rimColor = mix(accent, vec3(1.0), mix(0.5, 0.28, dark));\n color += rimColor * stroke(d, 0.013) * mix(0.34, 0.66, dark) * energy;\n /* An inner bevel a hair inside the wall, brightest along the top shoulder. */\n float bevel = stroke(d + 0.062, 0.02) * smoothstep(-0.35, 0.85, p.y / R);\n color += mix(accentCool, vec3(1.0), 0.5) * bevel * mix(0.2, 0.34, dark) * energy;\n /* Soft glow bleeding inward from the wall. */\n color += rimColor * exp(-abs(d) * 18.0) * mix(0.06, 0.13, dark) * energy * inside;\n\n /* ---- Sparks -------------------------------------------------------- */\n /* A dense fine mist rather than a handful of blobs. Phase and lane come from\n independent hashes — sharing one makes the field collapse into a visible\n lattice — and each mote carries its own size, speed and brightness so the\n drift never reads as one animated sheet. */\n vec3 sparks = vec3(0.0);\n for (int i = 0; i < 64; i++) {\n float fi = float(i);\n vec2 h = hash21(fi + 3.7);\n float phase = hash11(fi * 7.13 + 1.7);\n float weight = hash11(fi * 3.31 + 5.9);\n float life = fract(uTime * (0.2 + h.x * 0.42) * mix(0.35, 1.0, uOn) + phase);\n float x = mix(-L - R * 0.55, L + R * 0.55, life);\n float y = (h.y - 0.5) * 1.52 * (0.32 + 0.68 * sin(life * PI)) + sin(life * 6.4 + fi) * 0.06;\n vec2 sp = p - vec2(x, y);\n /* Stretch along travel so each one carries a comet tail. */\n sp.x *= 0.45;\n float fade = sin(life * PI);\n /* Tight kernels: the big ones were reading as glowing pills. */\n float grain = 1500.0 + 5200.0 * weight;\n float core = exp(-dot(sp, sp) * grain * 3.0);\n float tail = exp(-dot(sp, sp) * grain);\n float brightness = 0.35 + 0.65 * weight;\n sparks += mix(sparkColor, vec3(1.0), 0.6 * fade) * (tail * 0.55 + core) * fade * brightness;\n }\n color += sparks * mix(0.3, 1.15, uOn) * mix(0.85, 1.15, dark) * inside;\n\n /* ---- Thumb --------------------------------------------------------- */\n float thumbX = mix(-L, L, uProgress);\n vec2 tp = p - vec2(thumbX, 0.0);\n float td = length(tp) - TH;\n float thumbIn = fill(td);\n vec2 disc = tp / TH;\n float discLength = min(1.0, length(disc));\n vec3 normal = normalize(vec3(disc, sqrt(max(1e-4, 1.0 - discLength * discLength))));\n\n /* Micro relief on the shell, sampled in the sphere's own frame so it wraps\n with the surface instead of sliding across it. */\n vec2 shellUv = vec2(atan(normal.y, normal.x) * 1.6, normal.z * 2.4);\n float relief = valueNoise(shellUv * 15.0) * 0.6 + valueNoise(shellUv * 41.0) * 0.4;\n vec3 bumped = normalize(normal + vec3((relief - 0.5) * 0.035, (relief - 0.5) * 0.035, 0.0));\n\n vec3 light = normalize(vec3(-0.42 + uPointer.x * 0.35, 0.58 + uPointer.y * 0.35, 0.8));\n /* A cool fill from the opposite side, and a rim from behind: one lamp on a\n sphere gives a flat coin, three give it form. */\n vec3 fillDir = normalize(vec3(0.62, -0.28, 0.55));\n vec3 rimDir = normalize(vec3(0.34, 0.5, -0.72));\n float diffuse = max(0.0, dot(bumped, light));\n float specular = pow(max(0.0, dot(reflect(-light, bumped), vec3(0.0, 0.0, 1.0))), 120.0);\n /* A second, stretched lobe: the brushed anisotropic streak. */\n vec3 stretched = normalize(vec3(bumped.x * 0.28, bumped.y, bumped.z));\n float sheen = pow(max(0.0, dot(reflect(-light, stretched), vec3(0.0, 0.0, 1.0))), 16.0);\n float fillTerm = max(0.0, dot(bumped, fillDir));\n float rimTerm = pow(max(0.0, dot(bumped, rimDir)), 2.2) * pow(1.0 - max(0.0, normal.z), 1.6);\n float fresnel = pow(1.0 - max(0.0, normal.z), 2.6);\n\n vec3 thumbBase = mix(vec3(0.96, 0.97, 0.99), vec3(0.78, 0.82, 0.9), dark);\n vec3 thumbColor = thumbBase * (0.17 + 0.86 * diffuse * diffuse);\n thumbColor += thumbBase * mix(accentCool, vec3(1.0), 0.35) * fillTerm * 0.3;\n thumbColor += mix(accentCool, vec3(1.0), 0.5) * rimTerm * mix(0.3, 0.55, dark);\n thumbColor *= 0.975 + 0.05 * relief;\n thumbColor += mix(accent, accentWarm, 0.4) * fresnel * mix(0.34, 0.6, dark) * energy;\n thumbColor += vec3(1.0) * specular * 1.6;\n thumbColor += mix(vec3(1.0), accentCool, 0.35) * sheen * 0.13;\n thumbColor += mix(accent, accentWarm, 0.6) * uOn * 0.1;\n /* Roll the highlights off instead of letting them clip — a clipped sphere\n reads as a white sticker rather than a lit object. */\n /* Bounce off the lit trough onto the underside. */\n thumbColor += accentCool * smoothstep(0.2, -0.9, disc.y) * uOn * mix(0.1, 0.16, dark);\n thumbColor = thumbColor / (1.0 + thumbColor * 0.22);\n\n /* Contact shadow under the thumb. */\n float contact = exp(-dot(tp * vec2(0.8, 1.6), tp * vec2(0.8, 1.6)) * 1.9);\n color *= 1.0 - contact * 0.42 * inside;\n\n color = mix(color, thumbColor, thumbIn);\n /* Crisp terminator ring so the thumb keeps a defined edge. */\n color += mix(vec3(1.0), accentCool, 0.4) * stroke(td, 0.01) * mix(0.15, 0.26, dark);\n color += mix(accent, accentWarm, 0.35) * exp(-max(0.0, td) * 7.0) * uOn * 0.22 * inside;\n\n /* ---- Grade --------------------------------------------------------- */\n color *= mix(0.86, 1.0, smoothstep(1.0, 0.2, length(screen * vec2(1.0, 1.25))));\n float grain = hash22(gl_FragCoord.xy + vec2(mod(uTime * 61.0, 512.0), mod(uTime * 37.0, 512.0)));\n color += (grain - 0.5) * mix(0.014, 0.03, dark);\n\n gl_FragColor = vec4(max(color, 0.0), 1.0);\n}\n`;\n\nexport function buildShaderToggleFragment(hasDerivatives: boolean) {\n /* The extension pragma has to lead the source, before any other directive. */\n const prefix = hasDerivatives\n ? \"#extension GL_OES_standard_derivatives : enable\\n#define HAS_DERIVATIVES\\n\"\n : \"\";\n return prefix + SHADER_TOGGLE_FRAGMENT_BODY;\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/shader-toggle.css", + "language": "css", + "role": "variant-style", + "bytes": 2751, + "lines": 109, + "sha256": "c57012bddaaa4bfe1e18d19c23188f43d79e44fccf9d9e1e15454cd57e4f34cd", + "code": ".shader-toggle,\n.shader-toggle *,\n.shader-toggle *::before,\n.shader-toggle *::after {\n box-sizing: border-box;\n}\n\n.shader-toggle {\n --shader-toggle-hue: 0deg;\n --shader-toggle-saturation: 1;\n --shader-toggle-brightness: 1;\n --shader-toggle-stage: #f4f6fa;\n --shader-toggle-caption: rgba(17, 22, 34, 0.5);\n --shader-toggle-caption-on: rgba(17, 22, 34, 0.78);\n --shader-toggle-focus: rgba(58, 120, 255, 0.45);\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n isolation: isolate;\n container-type: size;\n background: var(--shader-toggle-stage);\n color-scheme: light;\n filter: hue-rotate(var(--shader-toggle-hue)) saturate(var(--shader-toggle-saturation)) brightness(var(--shader-toggle-brightness));\n font-family: ui-sans-serif, -apple-system, \"SF Pro Text\", \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n}\n\n.shader-toggle[data-mode=\"dark\"] {\n --shader-toggle-stage: #06070b;\n --shader-toggle-caption: rgba(233, 238, 250, 0.46);\n --shader-toggle-caption-on: rgba(233, 238, 250, 0.82);\n --shader-toggle-focus: rgba(122, 168, 255, 0.5);\n color-scheme: dark;\n}\n\n.shader-toggle__canvas {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n/* A real control sitting exactly over the drawn capsule, so the scene stays\n keyboard- and screen-reader-operable. */\n.shader-toggle__switch {\n position: absolute;\n left: 50%;\n top: 50%;\n width: 200px;\n height: 112px;\n padding: 0;\n border: 0;\n border-radius: 999px;\n background: transparent;\n transform: translate(-50%, -50%);\n cursor: pointer;\n appearance: none;\n}\n\n.shader-toggle__switch:focus-visible {\n outline: none;\n box-shadow: 0 0 0 4px var(--shader-toggle-focus);\n}\n\n.shader-toggle__caption {\n position: absolute;\n left: 50%;\n /* The control keeps 13% of the stage height, so the caption tracks the centre\n rather than the floor — otherwise it drifts away in a tall frame. */\n top: calc(50% + max(52px, 11cqh));\n transform: translateX(-50%);\n display: flex;\n align-items: baseline;\n gap: 0.55em;\n margin: 0;\n font-size: clamp(11px, 1.9cqw, 14px);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--shader-toggle-caption);\n transition: color 0.45s ease;\n pointer-events: none;\n}\n\n.shader-toggle[data-state=\"on\"] .shader-toggle__caption {\n color: var(--shader-toggle-caption-on);\n}\n\n.shader-toggle__caption b {\n font-weight: 500;\n}\n\n.shader-toggle__fallback {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n padding: 24px;\n text-align: center;\n font-size: 13px;\n line-height: 1.5;\n color: var(--shader-toggle-caption);\n}\n" + }, + { + "path": "src/shaders/skeuomorphic-toggle/toggleMode.ts", + "language": "typescript", + "role": "variant-module", + "bytes": 1870, + "lines": 46, + "sha256": "2ed0a1183b05b22c990f385d6e457bf6b3a11bf5749c84aae44ee9b1d0e406b8", + "code": "import { useEffect, useState } from \"react\";\n\nexport type ToggleMode = \"dark\" | \"light\";\nexport type ToggleModePreference = ToggleMode | \"auto\";\n\nexport function readAutomaticToggleMode(): ToggleMode {\n if (typeof document === \"undefined\" || typeof window === \"undefined\") return \"dark\";\n const root = document.documentElement;\n const declared = root.dataset.theme ?? root.dataset.scheme;\n if (declared === \"light\" || declared === \"dark\") return declared;\n if (root.classList.contains(\"light\")) return \"light\";\n if (root.classList.contains(\"dark\")) return \"dark\";\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/* Mirrors the packaged export's automatic appearance: the site's own\n data-theme/data-scheme attribute wins, then the system preference. */\nexport function useToggleMode(preference: ToggleModePreference): ToggleMode {\n const enabled = preference === \"auto\";\n const [automatic, setAutomatic] = useState(readAutomaticToggleMode);\n\n useEffect(() => {\n if (!enabled || typeof document === \"undefined\" || typeof window === \"undefined\") return undefined;\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const update = () => setAutomatic(readAutomaticToggleMode());\n const observer = new MutationObserver(update);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\", \"data-scheme\", \"data-theme\"],\n });\n media.addEventListener(\"change\", update);\n update();\n return () => {\n observer.disconnect();\n media.removeEventListener(\"change\", update);\n };\n }, [enabled]);\n\n if (preference === \"light\" || preference === \"dark\") return preference;\n return automatic;\n}\n\nexport function clamp(value: number, minimum: number, maximum: number) {\n return Math.min(maximum, Math.max(minimum, value));\n}\n" + }, { "path": "src/shaders/neuform-isolated/sources/skeuomorphic-toggle.html", "language": "html", diff --git a/scripts/generate-library-entry.mjs b/scripts/generate-library-entry.mjs index 450967a..46c0120 100644 --- a/scripts/generate-library-entry.mjs +++ b/scripts/generate-library-entry.mjs @@ -15,6 +15,18 @@ const exports = [ ...[...catalog.matchAll(eagerPattern)].map((match) => ({ path: match[2].replace("../", "./"), name: match[1] })), ]; +const compatibilityExports = [ + { name: "SkeuomorphicToggle", path: "./shaders/neuform-isolated/NeuformBatchEffects" }, +]; + +for (const compatibilityExport of compatibilityExports) { + const current = exports.find((item) => item.name === compatibilityExport.name); + if (current && current.path !== compatibilityExport.path) { + throw new Error(`Compatibility export ${compatibilityExport.name} moved from ${compatibilityExport.path} to ${current.path}.`); + } + if (!current) exports.push(compatibilityExport); +} + if (!exports.length) throw new Error("No Community renderer exports were found in src/data/shaders.tsx"); const names = new Set(); diff --git a/scripts/library-package.test.mjs b/scripts/library-package.test.mjs index 2b5dea0..651b7ad 100644 --- a/scripts/library-package.test.mjs +++ b/scripts/library-package.test.mjs @@ -9,6 +9,8 @@ const packageJson = JSON.parse(await readFile(resolve(root, "package.json"), "ut const cliPackageJson = JSON.parse(await readFile(resolve(root, "packages/cli/package.json"), "utf8")); const catalog = await readFile(resolve(root, "src/data/shaders.tsx"), "utf8"); const generator = await readFile(resolve(root, "scripts/generate-library-entry.mjs"), "utf8"); +const libraryEntry = await readFile(resolve(root, "src/index.ts"), "utf8"); +const legacyToggleEntry = await readFile(resolve(root, "src/package-components/SkeuomorphicToggle.ts"), "utf8"); test("the npm package is public and exports ESM, types, CSS, and assets", () => { assert.equal(packageJson.name, "@designcodeio/threeui"); @@ -33,3 +35,11 @@ test("the generated library surface is derived only from the Community catalog i assert.doesNotMatch(catalog, /\bProRenderer\b|access:\s*["']pro["']/i); assert.match(catalog, /CommunityRenderer1/); }); + +test("the expanded toggle collection preserves the published toggle imports", () => { + assert.match(generator, /compatibilityExports/); + assert.match(generator, /name: "SkeuomorphicToggle"/); + assert.match(libraryEntry, /export \{ SkeuomorphicToggle \} from "\.\/package-components\/SkeuomorphicToggle"/); + assert.match(libraryEntry, /export \{ SkeuomorphicToggleCollection \} from "\.\/package-components\/SkeuomorphicToggleCollection"/); + assert.match(legacyToggleEntry, /export \{ SkeuomorphicToggle \} from "\.\.\/shaders\/neuform-isolated\/NeuformBatchEffects"/); +}); diff --git a/scripts/package-install-smoke.mjs b/scripts/package-install-smoke.mjs index 676f359..d09e3e8 100644 --- a/scripts/package-install-smoke.mjs +++ b/scripts/package-install-smoke.mjs @@ -41,7 +41,11 @@ try { ); execFileSync( process.execPath, - ["--input-type=module", "--eval", "const root = await import('@designcodeio/threeui'); const subpath = await import('@designcodeio/threeui/components/CloudField'); if (Object.keys(root).length < 1 || typeof subpath.CloudField !== 'function') process.exit(1);"], + [ + "--input-type=module", + "--eval", + "const root = await import('@designcodeio/threeui'); const cloud = await import('@designcodeio/threeui/components/CloudField'); const legacyToggle = await import('@designcodeio/threeui/components/SkeuomorphicToggle'); const toggleCollection = await import('@designcodeio/threeui/components/SkeuomorphicToggleCollection'); if (typeof root.SkeuomorphicToggle !== 'function' || typeof root.SkeuomorphicToggleCollection !== 'function' || typeof cloud.CloudField !== 'function' || typeof legacyToggle.SkeuomorphicToggle !== 'function' || typeof toggleCollection.SkeuomorphicToggleCollection !== 'function') process.exit(1);", + ], { cwd: consumerDirectory, stdio: "inherit", env: environment }, ); console.log(`Anonymous install smoke test passed for ${packResult[0].filename}.`); diff --git a/src/browseTaxonomy.js b/src/browseTaxonomy.js index 53cfee5..7e7d604 100644 --- a/src/browseTaxonomy.js +++ b/src/browseTaxonomy.js @@ -50,11 +50,6 @@ const CATEGORY_CONTENT = Object.freeze({ heading: "Web motion design components", description: "Browse motion design components for interactive websites, including cinematic transitions, responsive animation, and production-ready source.", }), - Sections: Object.freeze({ - title: "Interactive Website Sections", - heading: "Interactive website sections", - description: "Browse reusable website sections with interactive layouts, polished motion, responsive behavior, and copy-ready implementation source.", - }), }); const CATEGORY_FAQS = Object.freeze({ @@ -150,6 +145,42 @@ const CATEGORY_FAQS = Object.freeze({ ]), }); +// Keep tag URLs useful as browse filters without automatically asking search +// engines to index every internal catalog descriptor. This list is deliberately +// limited to distinct technologies and visual techniques with substantive +// collections; category synonyms belong to their dedicated category routes. +export const INDEXABLE_BROWSE_TAGS = Object.freeze([ + "3d", + "canvas", + "canvas2d", + "crt", + "flow field", + "fluid", + "glsl", + "gsap", + "halftone", + "hover", + "isometric", + "noise", + "parallax", + "particles", + "physics", + "point cloud", + "postprocessing", + "procedural", + "refraction", + "scroll", + "scrolltrigger", + "shader", + "shadermaterial", + "tailwind", + "typography", + "webgl", + "webgl2", +]); + +const INDEXABLE_BROWSE_TAG_SET = new Set(INDEXABLE_BROWSE_TAGS); + const TOKEN_LABELS = Object.freeze({ "2d": "2D", "3d": "3D", @@ -193,6 +224,10 @@ export function browseTagLabel(tag) { .join(" "); } +export function isIndexableBrowseTag(tag) { + return typeof tag === "string" && INDEXABLE_BROWSE_TAG_SET.has(tag.trim().toLowerCase()); +} + export function browseTagContent(tag, resultCount = 0) { const label = browseTagLabel(tag); const countText = resultCount > 0 ? `${resultCount} ` : ""; diff --git a/src/components/buildCopyBundles.js b/src/components/buildCopyBundles.js index 97b43b4..d845fa9 100644 --- a/src/components/buildCopyBundles.js +++ b/src/components/buildCopyBundles.js @@ -41,19 +41,38 @@ function fullSourceSection(files) { ]; } -function sourceReferenceSection(files, sourceBaseUrl, sourcePageUrl) { - const primary = files.find((file) => ["canonical-source", "scene-source"].includes(file.role)) ?? files[0]; - if (!primary) return ["No source file is registered for this component.", ""]; - - const filename = primary.path.split("/").at(-1) ?? primary.path; - const directUrl = primary.sourceUrl ? new URL(primary.sourceUrl, sourceBaseUrl).href : sourcePageUrl; - +function sourceManifestSection(files) { + if (files.length === 0) return ["No source file is registered for this component.", ""]; return [ - `### Source code: [${filename}](${directUrl})`, + ...files.map((file) => `- \`${file.path}\` — ${file.role} · SHA-256 \`${file.sha256}\``), "", ]; } +function directHtmlSource(files, sourceBaseUrl) { + const file = files.find((candidate) => { + if (!["canonical-source", "scene-source"].includes(candidate.role) || !candidate.sourceUrl) return false; + try { + return new URL(candidate.sourceUrl, sourceBaseUrl).pathname.toLowerCase().endsWith(".html"); + } catch { + return false; + } + }); + if (!file) return null; + return { + filename: file.path.split("/").at(-1) ?? file.path, + url: new URL(file.sourceUrl, sourceBaseUrl).href, + }; +} + +export function sourceBundleCandidateIds({ shaderId, activeVariantId, directVariantId }) { + return [...new Set([ + directVariantId, + activeVariantId ? `${shaderId}--${activeVariantId}` : undefined, + shaderId, + ].filter(Boolean))]; +} + export function buildPromptSourceFallback(shader) { const files = (shader.sourceFiles ?? []).flatMap((sourceFile, index) => { const path = String(sourceFile).split(" — ", 1)[0].trim(); @@ -81,14 +100,34 @@ export function buildPromptSourceFallback(shader) { }]; } -export function buildCodeBundle({ shader, files, assets }) { +/** + * @param {{ + * shader: any; + * files: any[]; + * assets: any[]; + * usage?: string; + * activeVariant?: any; + * }} options + */ +export function buildCodeBundle(options) { + const { shader, files, assets, usage, activeVariant } = options; + const normalizedUsage = usage ?? `<${shader.importName} />`; + const usageFence = markdownFence(normalizedUsage); + const variantLabel = activeVariant ? ` — ${activeVariant.label}` : ""; return [ - `# ${shader.label} — Complete source`, + `# ${shader.label}${variantLabel} — Complete source`, "", `Component: \`${shader.importName}\``, + ...(activeVariant ? [`Variant: **${activeVariant.label}** (\`${activeVariant.id}\`)`] : []), `Runtime: ${shader.runtime}`, `Source revision: \`${shader.sourceCommit}\``, "", + "## Current configured usage", + "", + `${usageFence}tsx`, + normalizedUsage, + usageFence, + "", "## Required assets", "", ...assetSection(assets), @@ -98,28 +137,87 @@ export function buildCodeBundle({ shader, files, assets }) { ].join("\n"); } -export function buildCopyPrompt({ shader, files, activeVariant, sourceBaseUrl, sourcePageUrl }) { +/** + * @param {{ + * shader: any; + * files: any[]; + * assets?: any[]; + * usage?: string; + * activeVariant?: any; + * sourceBaseUrl?: string; + * sourcePageUrl?: string; + * sourceBundleUrl?: string; + * includeInlineSource?: boolean; + * }} options + */ +export function buildCopyPrompt(options) { + const { + shader, + files, + assets, + usage, + activeVariant, + sourceBaseUrl, + sourcePageUrl, + sourceBundleUrl, + includeInlineSource = false, + } = options; + const normalizedAssets = assets ?? []; + const normalizedUsage = usage ?? `<${shader.importName} />`; const variantLines = activeVariant ? [ - `### Variant: ${activeVariant.label}`, + `Variant: **${activeVariant.label}** (\`${activeVariant.id}\`)`, ] : []; + const referenceBrief = activeVariant?.description ?? shader.description; + const canonicalHtml = directHtmlSource(files, sourceBaseUrl ?? sourcePageUrl); + const sourceLinks = [ + ...(canonicalHtml ? [`Canonical HTML: [${canonicalHtml.filename}](${canonicalHtml.url})`] : []), + ...(sourceBundleUrl ? [`Complete registered source bundle: [${sourceBundleUrl}](${sourceBundleUrl})`] : []), + ]; + const usageFence = markdownFence(normalizedUsage); + const inlineFiles = includeInlineSource ? files.filter((file) => typeof file.code === "string") : []; + const unavailableSourceMessage = inlineFiles.length > 0 + ? "This is protected source supplied by the authenticated ThreeUI page in the inline bundle below." + : "The exact source is available only at the registered local paths below. Read those files from the current repository; if they are unavailable, stop instead of recreating them."; + return [ - `## Use the <${shader.importName} /> component from ThreeUI as a reference`, + `# Integrate <${shader.importName} /> from ThreeUI using its exact source`, "", - "You are working in an existing application. Use the following ThreeUI example as a visual, interaction, and implementation reference for the requested experience.", + "You are working in an existing application. Implement this component from the exact source linked or included below. Do not recreate it from the preview, screenshot, description, or filename.", "", - `### Component: ${shader.importName}`, + `Component: \`${shader.importName}\``, ...variantLines, - `### Runtime: ${shader.runtime}`, - ...sourceReferenceSection(files, sourceBaseUrl, sourcePageUrl), - "---", + `Runtime: ${shader.runtime}`, + ...(shader.sourceCommit ? [`Source revision: \`${shader.sourceCommit}\``] : []), + ...(referenceBrief ? ["", "Reference brief:", referenceBrief] : []), + "", + "## Current configured usage", + "", + `${usageFence}tsx`, + normalizedUsage, + usageFence, + "", + "## Exact implementation source", + "", + ...(sourceLinks.length > 0 ? [...sourceLinks, ""] : [unavailableSourceMessage, ""]), + "Required registered files:", "", - "### Reference brief", + ...sourceManifestSection(files), + ...(normalizedAssets.length > 0 ? ["Required binary assets:", "", ...assetSection(normalizedAssets)] : []), + "## Implementation requirements", "", - shader.description, + `- Preserve the authored structure, styling, shaders, motion, interactions, responsive behavior, dependencies, and asset paths described by the source.`, + `- Use the configured \`<${shader.importName} />\` usage above, including the selected variant and props.`, + "- Build directly in the destination project. Do not embed the ThreeUI documentation page and do not approximate the result from its rendered appearance.", + "- Fetch and read the complete source before editing. If the source cannot be retrieved, stop and report that instead of recreating it.", + "- After implementation, verify the rendered result and its interactions in the browser.", "", - "Review the linked source before making changes. Study its structure, styling, composition, motion, interactions, responsive behavior, and implementation details. Adapt the relevant ideas to the destination project's existing stack, content, and design language. Build the result directly in that project rather than embedding the reference URL.", + ...(inlineFiles.length > 0 ? [ + "## Authenticated full implementation source", + "", + ...fullSourceSection(inlineFiles), + ] : []), "", ].join("\n"); } diff --git a/src/components/buildSkillMarkdown.js b/src/components/buildSkillMarkdown.js index c4bd95a..f910552 100644 --- a/src/components/buildSkillMarkdown.js +++ b/src/components/buildSkillMarkdown.js @@ -6,7 +6,7 @@ const SKILLS = { "meng-to-sketchbook-landing-page": "---\nname: add-meng-to-sketchbook-landing-page\ndescription: \"Build Sketchbook from its verified authored source using Full HTML + DOM/CSS + JavaScript, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Sketchbook\n\n## Description\n\nA tactile personal portfolio built as a Singapore sketchbook, with nine illustrated plates, curled page turns, a draggable magnifying glass, zoom controls, a botanical paper atmosphere, and an editorial index.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React iframe host\n- Byte-exact complete authored HTML document\n- Same-project local source URL\n- Fourteen local paper, botanical, and Singapore illustration images plus three local Instrument Serif and Newsreader font files\n\n## Verified source material\n\n- `src/shaders/landing-pages/LandingPages.tsx`\n- `src/shaders/landing-pages/pageTypography.ts`\n- `src/shaders/landing-pages/pageRecipes.ts`\n- `public/landing-pages/meng-to-sketchbook.html — byte-exact complete page`\n- `public/landing-pages/meng-to-sketchbook/ — 17 local image and font assets`\n\nSource revision: `SHA-256 e0330548b1ac`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Copy the complete Meng To Sketchbook HTML file byte-for-byte to /landing-pages/meng-to-sketchbook.html; do not extract, rewrite, shorten, or rebrand any section.\n3. Preserve every embedded style, script, media payload, text string, interaction, responsive rule, and document-level lifecycle.\n4. Keep every relative local asset at the exact path expected by the original document.\n5. Load the local document in a full-size iframe whose permissions retain the authored forms, modals, downloads, popups, scripts, and same-origin resources.\n6. Lazy-load only the React host bundle; do not import the complete HTML into the application JavaScript graph.\n7. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: Copy meng-to-sketchbook.html byte-for-byte and keep all 17 files under meng-to-sketchbook/ at exactly that relative path. The page needs no network access.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { MengToSketchbookLandingPage } from \"./effects/meng-to-sketchbook-landing-page/MengToSketchbookLandingPage\";\nimport \"./effects/meng-to-sketchbook-landing-page/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\n\n```\n\n## Behavior contract\n\n- Runtime: Full HTML + DOM/CSS + JavaScript\n- Passes: 1 sandboxed full-document renderer\n- Interaction: Original page turns, pointer tilt, draggable magnifier, zoom controls, navigation, scroll, keyboard, and responsive layout\n- Assets: Fourteen local paper, botanical, and Singapore illustration images plus three local variable and display fonts\n- **document** (fixed): Complete original meng-to-sketchbook.html, byte-for-byte\n- **sourceUrl** (fixed): /landing-pages/meng-to-sketchbook.html\n- **headingFont** (optional): Instrument Serif | Newsreader | Geist\n- **bodyFont** (optional): Newsreader | Geist | Instrument Serif\n- **headingWeight** (optional): 300 | 400 | 500 | 600\n- **bodyWeight** (optional): 200 | 300 | 400 | 500 | 600\n- **primaryColor** (optional): Hex color — page ink and its opacity tints; the warm earth accent remains authored\n- **typography** (optional): Nameplate and plate-title scale + body scale + heading tracking\n- **layout** (responsive): Original full landing page inside the preview frame\n- **interaction** (original): Page turns + magnifier drag + zoom + pointer tilt + scroll + keyboard\n- **assets** (local): 17 packaged files in public/landing-pages/meng-to-sketchbook/; no network request\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", "predictive-arc": "---\nname: add-predictive-arc\ndescription: \"Build Predictive Arc from its verified authored source using Canvas 2D + Raw WebGL + Three.js r128, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Predictive Arc\n\n## Description\n\nEight animated arc, signal, ribbon, void, and halftone scenes collected in one Canvas 2D, raw-WebGL, and Three.js family.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React variant host\n- Four Canvas 2D renderers\n- Three raw-WebGL renderers\n- One Three.js point-field renderer\n- Dark/light mode surfaces\n- Lazy-loaded variant sources\n- ResizeObserver, IntersectionObserver, adaptive pixel ratio, and requestAnimationFrame\n\n## Verified source material\n\n- `Axiom---Predictive-Search-Engine (3).html — predictive arc source`\n- `Axiom-Dynamic-Data-Orchestration.html — data pixel arc source`\n- `src/shaders/predictive-arc/predictiveArcRenderer.ts`\n- `src/shaders/data-pixel-arc/dataPixelArcRenderer.ts`\n- `src/shaders/neuform-isolated/sources/signal-particles.html`\n- `src/shaders/neuform-isolated/sources/override-grid.html`\n- `src/shaders/neuform-isolated/NeuformBatchEffects.tsx`\n- `src/shaders/ribbon-field/RibbonFieldBackground.tsx`\n- `src/shaders/ribbon-field/ribbonFieldShaders.ts`\n- `src/shaders/neuform-isolated/NeuformIsolatedEffects.tsx`\n- `src/shaders/neuform-isolated/sources/void-protocol.html`\n- `src/shaders/neuform-isolated/NeuformCraftEffects.tsx`\n- `src/shaders/neuform-isolated/sources/nexus-unified-flow.html`\n- `src/shaders/neuform-isolated/sources/amber-halftone.html`\n- `src/shaders/predictive-arc/PredictiveArcCollection.tsx`\n- `src/shaders/predictive-arc/PredictiveArcCanvas.tsx`\n\nSource revision: `SHA-256 fa86582fc870`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Keep Predictive Arc as the public entry point and select predictive, data-pixel, signal-particles, override-grid, ribbon-field, void-field, halftone-flow, or amber-halftone with the variant prop.\n3. Retain each authored renderer and its original composition instead of blending the scenes into one canvas.\n4. Expose the shared mode, speed, hue, saturation, and brightness controls at the collection boundary.\n5. Lazy-load the isolated Signal Particles, Override Grid, Ribbon Field, Void Field, Halftone Flow, and Amber Halftone renderers so only the selected variant runs.\n6. Preserve each renderer's resize, visibility, animation-frame, and cleanup lifecycle.\n7. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { PredictiveArcCanvas } from \"./effects/predictive-arc/PredictiveArcCanvas\";\nimport \"./effects/predictive-arc/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\n\n```\n\n## Behavior contract\n\n- Runtime: Canvas 2D + Raw WebGL + Three.js r128\n- Passes: 1 selected Canvas 2D, raw-WebGL, or Three.js field pass\n- Interaction: Variant selection plus customizable mode, speed, color, and brightness\n- Assets: No external assets\n- **renderer** (host): Canvas 2D + Raw WebGL + Three.js\n- **variants** (fixed): Predictive + Data Pixel + Signal + Override + Ribbon + Void + Halftone Flow + Amber Halftone\n- **mode** (optional): dark | light\n- **pixelRatio** (adaptive): ≤ 2\n- **assets** (fixed): None\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", "liquid-form": "---\nname: add-liquid-form\ndescription: \"Build Liquid Form from its verified authored source using Raw WebGL, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Liquid Form\n\n## Description\n\nA centered silver ray-marched liquid form with authored studio reflections and pointer-responsive camera drift.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React lifecycle host\n- Raw WebGL 1 fullscreen quad\n- High-precision GLSL simplex noise and 70-step signed-distance-field ray march\n- Smoothed pointer look-at and adaptive 1.5x DPR\n\n## Verified source material\n\n- `V-E-L-O-X-Quantum-Engineered.html — WebGL background`\n- `src/shaders/liquid-form/liquidFormShaders.ts`\n- `src/shaders/liquid-form/LiquidFormBackground.tsx`\n\nSource revision: `SHA-256 acc0cbacb914`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Compile the exact full-screen vertex shader and complete silver liquid metal fragment shader.\n3. Keep all three noise octaves, SDF normals, environment lights, Fresnel, specular, bloom, tone mapping, and gamma stages.\n4. Map geometric customization to uniforms, then optionally tint the final silver canvas; tint amount zero must remain source-exact.\n5. Preserve pointer smoothing, resize, visibility pausing, context ownership, and complete WebGL resource cleanup.\n6. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { LiquidFormBackground } from \"./effects/liquid-form/LiquidFormBackground\";\nimport \"./effects/liquid-form/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\ngl.uniform1f(uniforms.morph, options.morph);\ngl.uniform1f(uniforms.noiseScale, options.noiseScale);\ngl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n```\n\n## Behavior contract\n\n- Runtime: Raw WebGL\n- Passes: 1 raw WebGL ray-march pass\n- Interaction: Smoothed pointer look-at plus customizable material, morph, and tint\n- Assets: No external assets\n- **renderer** (host): Raw WebGL\n- **steps** (fixed): 70\n- **pixelRatio** (adaptive): ≤ 1.5\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", - "crt": "---\nname: add-crt\ndescription: \"Build CRT from its verified authored source using Raw WebGL + Canvas 2D, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build CRT\n\n## Description\n\nA complete Matrix-era boot terminal rendered to an offscreen text texture and passed through the exact authored curved CRT shader.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React lifecycle host\n- Raw WebGL CRT composite\n- Offscreen Canvas 2D boot-text texture\n- Visibility-aware animation and adaptive backing resolution\n\n## Verified source material\n\n- `ZION-Construct-Initialization (1).html — complete CRT background`\n- `src/shaders/crt/crtRenderer.ts`\n- `src/shaders/crt/crtShaders.ts`\n- `src/shaders/crt/CrtBackground.tsx`\n\nSource revision: `SHA-256 860a1eb1d4c9`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Keep the complete 19-row terminal log, colors, layout math, cursor, reveal cadence, and blink state.\n3. Upload the offscreen Canvas texture only when the boot text changes.\n4. Compile the exact authored curvature, chromatic offset, scanline, grille, rolling bar, vignette, flicker, and grain GLSL.\n5. Apply brightness, opacity, and a hue/saturation grade to the completed host rather than editing the texture or GLSL.\n6. Own texture, buffer, shader, observer, resize, frame, and disposal lifecycle.\n7. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { CrtBackground } from \"./effects/crt/CrtBackground\";\nimport \"./effects/crt/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\ndrawScreen(reveal);\nif (cursorVisible) drawCursor();\ngl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);\ngl.drawArrays(gl.TRIANGLES, 0, 3);\n```\n\n## Behavior contract\n\n- Runtime: Raw WebGL + Canvas 2D\n- Passes: 2 — Canvas 2D boot texture + raw WebGL CRT composite\n- Interaction: Customizable boot speed, CRT motion, hue, brightness, and opacity\n- Assets: No external assets\n- **renderer** (host): Raw WebGL + Canvas 2D\n- **boot** (fixed): 19 authored terminal rows\n- **assets** (fixed): None\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", + "crt": "---\nname: add-crt\ndescription: \"Build CRT from its verified authored source using Raw WebGL + Canvas 2D, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build CRT\n\n## Description\n\nOne sharpened curved-glass CRT tube driving four screens: the Matrix-era boot terminal, a monochrome film leader, a noise-torn blue signal fault, and an 8-bit console title.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React lifecycle host\n- Raw WebGL CRT composite\n- Offscreen Canvas 2D screen texture\n- Hand-authored 5x7 pixel font\n- Visibility-aware animation and adaptive backing resolution\n\n## Verified source material\n\n- `ZION-Construct-Initialization (1).html — complete CRT background`\n- `src/shaders/crt/crtRenderer.ts`\n- `src/shaders/crt/crtShaders.ts`\n- `src/shaders/crt/crtScreens.ts`\n- `src/shaders/crt/CrtBackground.tsx`\n\nSource revision: `SHA-256 860a1eb1d4c9`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Size the backing store to the host at up to 1920 px and 2x device pixel ratio, and pin the scanline count and aperture-grille pitch to CSS size so the authored look survives the higher resolution.\n3. Select the screen with the variant prop: the complete 19-row terminal log with its colors, layout math, cursor, reveal cadence, and blink state; a monotone film leader; a blue signal fault; or an 8-bit console title.\n4. Give each screen the texture resolution it needs — full backing for type, a 320x180 nearest-filtered grid for pixel art — and upload only when that screen changes.\n5. Compile one fragment shader whose curvature, chromatic offset, halation, scanline, grille, transport noise, rolling bar, vignette, flicker, monochrome, and grain stages are driven by the variant's uniform preset.\n6. Apply brightness, opacity, and a hue/saturation grade to the completed host rather than editing the texture or GLSL.\n7. Own texture, buffer, shader, observer, resize, frame, and disposal lifecycle.\n8. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { CrtBackground } from \"./effects/crt/CrtBackground\";\nimport \"./effects/crt/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\n\n\nCRT_SCREENS[variant](textContext, width, height, seconds);\ngl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);\ngl.drawArrays(gl.TRIANGLES, 0, 3);\n```\n\n## Behavior contract\n\n- Runtime: Raw WebGL + Canvas 2D\n- Passes: 2 — Canvas 2D screen texture + raw WebGL CRT composite\n- Interaction: Selectable screen style plus customizable boot speed, CRT motion, hue, brightness, and opacity\n- Assets: No external assets\n- **renderer** (host): Raw WebGL + Canvas 2D\n- **variants** (fixed): Terminal | Cinematic | Blue Screen | Nintendo\n- **backing** (adaptive): ≤ 1920 px, ≤ 2 DPR\n- **assets** (fixed): None\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", "energy-orb": "---\nname: add-energy-orb\ndescription: \"Build Globe from its verified Community source with the complete renderer, interactions, controls, and required assets.\"\n---\n\n# Build Globe\n\nThe original layered FBM energy sphere with translucent rim glow and depth-aware star field.\n\n## Verified source material\n\n- `NXA-Decentralized-AI-Compute-Protocol (2).html — WebGL energy orb`\n- `src/shaders/globe/GlobeCollection.tsx`\n\nSource revision: `SHA-256 03b1b8e2c440 / 3de7fdcb6239 / 7a88f26e4d8`\n\n## Requirements\n\n- Preserve the complete Raw WebGL + Canvas 2D renderer and every published Community option.\n- Preserve its interaction contract: Pointer-responsive energy sphere with visibility-aware animation and customizable motion, scale, palette, and opacity.\n- Preserve its assets: No runtime assets.\n- Verify resize, mobile, reduced motion, visibility, teardown, and console health.\n", "spark-badge": "---\nname: add-spark-badge\ndescription: \"Build Spark Badge from its verified Community source with the complete renderer, interactions, controls, and required assets.\"\n---\n\n# Build Spark Badge\n\nA luminous credential badge held together by curl-noise embers, carved typography, rain occlusion, waterline sparks, and an adaptive particle field.\n\n## Verified source material\n\n- `spark-badge.html — complete self-contained Canvas 2D particle scene`\n- `src/shaders/spark-badge/spark-badge.html`\n- `src/shaders/spark-badge/SparkBadge.tsx`\n\nSource revision: `SHA-256 a8eefdee0d87`\n\n## Requirements\n\n- Preserve the complete Canvas 2D renderer and every published Community option.\n- Preserve its interaction contract: Live speed, spark, rain, turbulence, and spread controls over the ambient 17-second dissolve/reform cycle.\n- Preserve its assets: One self-contained authored HTML badge scene; no external assets.\n- Verify resize, mobile, reduced motion, visibility, teardown, and console health.\n", "elements": "---\nname: add-elements\ndescription: \"Build Elements from its verified authored source using Raw WebGL2 + Canvas 2D, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Elements\n\n## Description\n\nWater, lightning, fire, condensation, and a painterly generative tree collected as one elemental family across WebGL2 and Canvas 2D.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React variant host\n- Three focused raw WebGL2 Elemental Marks panels\n- Painterly Canvas 2D Generative Tree\n- Existing transparent Canvas 2D Condensation renderer\n- Allow-scripts-only source sandbox\n- Lazy-loaded variant boundaries\n\n## Verified source material\n\n- `elemental-marks.html — complete water, lightning, and fire source`\n- `src/shaders/elements/sources/elemental-marks.html`\n- `src/shaders/elements/ElementsBackground.tsx`\n- `src/shaders/elements/ElementsCollection.tsx`\n- `generative-tree.html — complete authored Canvas 2D renderer`\n- `src/shaders/elements/sources/generative-tree.html`\n- `src/shaders/elements/GenerativeTree.tsx`\n- `src/shaders/condensation/condensationRenderer.ts`\n- `src/shaders/condensation/CondensationBackground.tsx`\n\nSource revision: `SHA-256 7a6871fe99fa`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Keep ElementsCollection as the public entry point and select water, lightning, fire, condensation, or generative-tree with the variant prop.\n3. Package the complete Elemental Marks HTML byte-for-byte, focus one authored panel, and apply the documented higher-resolution scale and detail refinements only in the React presentation adapter while preserving its mark paths and pointer behavior.\n4. Lazy-load Condensation through its existing renderer and Generative Tree through its byte-exact Canvas 2D sandbox so each lifecycle remains independent.\n5. Expose the shared speed, size, particles, opacity, and palette controls for WebGL2 marks and Generative Tree while keeping Condensation's speed, drops, and opacity controls.\n6. Mount only the selected family component and preserve sandbox, visibility, reduced-motion, resize, pointer, and cleanup lifecycles.\n7. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { ElementsCollection } from \"./effects/elements/ElementsCollection\";\nimport \"./effects/elements/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\n\n```\n\n## Behavior contract\n\n- Runtime: Raw WebGL2 + Canvas 2D\n- Passes: 1 selected composition — up to 3 WebGL2 passes or 1 Canvas 2D pass\n- Interaction: Variant selection, pointer-reactive marks and tree wind, speed, scale, particles, palette, and opacity\n- Assets: Three embedded vector brand paths; no external binary assets\n- **renderer** (host): Sandboxed WebGL2 or Canvas 2D\n- **variants** (fixed): Water + Lightning + Fire + Condensation + Generative Tree\n- **source** (fixed): Complete authored Elemental Marks + Generative Tree documents\n- **assets** (embedded): Three vector mark paths; no external tree assets\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", @@ -28,17 +28,17 @@ const SKILLS = { "structure-flow": "---\nname: add-structure-flow\ndescription: \"Build Structure Flow from its verified authored source using Three.js r128–r160, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Structure Flow\n\n## Description\n\nThirteen authored Three.js field studies collected as one family, spanning particle domes, horizons, orbital systems, matrices, topology, fluid fields, embers, and vortexes.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React typed variant host\n- Thirteen authored Three.js r128-r160 field renderers\n- Point clouds, ShaderMaterials, topology scenes, fluid fields, embers, and post-process bloom\n- Lazy-loaded renderer-specific lifecycles and controls\n\n## Verified source material\n\n- `src/shaders/structure-flow/StructureFlowCollection.tsx`\n- `Axiom-Structure-Flow (2).html — Three.js background`\n- `src/shaders/structure-flow/structureFlowRenderer.ts`\n- `src/shaders/structure-flow/StructureFlowBackground.tsx`\n- `src/shaders/emerald-horizon/EmeraldHorizonBackground.tsx`\n- `src/shaders/orbital-sphere/OrbitalSphereBackground.tsx`\n- `src/shaders/dot-matrix/DotMatrixBackground.tsx`\n- `src/shaders/neuform-isolated/NeuformIsolatedEffects.tsx`\n- `src/shaders/neuform-isolated/NeuformCraftEffects.tsx`\n- `src/shaders/neuform-isolated/NeuformBatchEffects.tsx`\n\nSource revision: `SHA-256 40eb5bac81e3`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Use StructureFlowCollection as the family entry point and select the exact authored renderer with the variant prop.\n3. Keep Structure Flow, Emerald Horizon, Orbital Sphere, Dot Matrix, Expanse Field, Logic Core, Dimensional Field, Data Field, Topology Field, Nebula, Fluid Field, Ember Storm, and Flux Vortex as independent scenes rather than blending them into one renderer.\n4. Preserve the source-exact Three.js revision, geometry, shaders, camera, palette, motion, and pointer behavior for every variant.\n5. Expose each renderer's own controls at the variant boundary and lazy-load only the selected implementation.\n6. Retain every source renderer's resize, visibility, animation-frame, context, and disposal lifecycle.\n7. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { StructureFlowCollection } from \"./effects/structure-flow/StructureFlowCollection\";\nimport \"./effects/structure-flow/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\n\n```\n\n## Behavior contract\n\n- Runtime: Three.js r128–r160\n- Passes: 1–2 Three.js scene, point-cloud, or ShaderMaterial passes\n- Interaction: Variant-specific pointer, motion, geometry, opacity, mask, and palette controls\n- Assets: No external assets\n- **renderer** (variant): Three.js r128–r160\n- **variants** (fixed): 13 field studies\n- **controls** (adaptive): Renderer-specific\n- **assets** (fixed): None\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", "warp-field": "---\nname: add-warp-field\ndescription: \"Build Warp Field from its verified authored source using Three.js r128, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Warp Field\n\n## Description\n\nNexus’s focused hero warp: 400 emerald additive streaks and 40 luminous tiles streaming through an authored deep-space fog field.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React lifecycle host\n- Pinned Three.js r128\n- 400 colored additive line segments\n- 40 luminous plane tiles and exponential fog\n\n## Verified source material\n\n- `Nexus-Edge-Compute.html — hero warp scene`\n- `src/shaders/warp-field/warpFieldRenderer.ts`\n- `src/shaders/warp-field/WarpFieldBackground.tsx`\n\nSource revision: `SHA-256 bd7c486164d8`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Pin Three.js 0.128.0 and carry only the complete primary hero-warp scene.\n3. Retain the exact random distributions, four-color palette, line lengths, tile geometry, materials, camera, fog, and wrap bounds.\n4. Advance both line endpoints and all tiles by the same authored speed so the warp stays coherent.\n5. Expose speed, opacities, FOV, brightness, and a final-canvas hue/saturation grade with source-exact defaults.\n6. Dispose cloned tile materials, both geometries, the streak material, observers, frames, and renderer.\n7. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { WarpFieldBackground } from \"./effects/warp-field/WarpFieldBackground\";\nimport \"./effects/warp-field/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\npositions[index * 6 + 2] += speed;\npositions[index * 6 + 5] += speed;\nif (positions[index * 6 + 2] > 200) wrapStreak(index);\nrenderer.render(scene, camera);\n```\n\n## Behavior contract\n\n- Runtime: Three.js r128\n- Passes: 1 Three.js scene render\n- Interaction: Customizable speed, streaks, tiles, color, camera, and brightness\n- Assets: No external assets\n- **renderer** (host): Three.js r128\n- **variant** (optional): streaks | letters | keycaps | hyperspace\n- **streaks** (fixed): 400\n- **tiles** (fixed): 40\n- **assets** (fixed): None\n\n## Verification\n\n1. Compare the rendered composition, animation timing, pointer behavior, and state transitions with the source implementation.\n2. Exercise resize, high-DPI, mobile/coarse-pointer, reduced-motion, tab visibility, and WebGL context-loss paths where applicable.\n3. Confirm every animation frame, observer, listener, geometry, buffer, texture, framebuffer, material, and renderer is released on teardown.\n4. Check the browser console and confirm the effect renders at native-or-better backing resolution.\n\n## Guardrails\n\n- Do not substitute a visually similar package, demo, shader, or runtime.\n- Do not approximate, reconstruct, or simplify the authored GLSL, render passes, geometry, interaction state, or assets.\n- Keep exact source and asset hashes under regression tests when the source project provides them.\n- Adapt only the surrounding host boundary needed by the target project; keep renderer behavior intact.\n", "engraved-certificate": "---\nname: add-engraved-certificate\ndescription: \"Build Engraved Certificate from its verified authored source using Canvas 2D + DOM/CSS, including the complete renderer, interactions, and required assets. Use when Codex needs to implement, port, or adapt this effect without requiring the ThreeUI package or reconstructing the visual from an approximation.\"\n---\n\n# Build Engraved Certificate\n\n## Description\n\nA responsive engraved certificate: plate field, dual guilloche rosettes, and a drifting harmonic pass that auto-cycles through four cam states.\n\nRecreate the authored behavior from the verified source, not from screenshots or the abbreviated orchestration sample in this skill. The implementation may live directly in the target project and does not require `@designcodeio/threeui`.\n\n## Technologies\n\n- React component with a sandboxed `srcDoc` effect boundary\n- Canvas 2D engraving passes and certificate DOM copied from the byte-exact Neuform export\n- A post-load focus adapter that keeps only the authored shader, button, canvas, or visual targets visible\n- Optional outer-frame hue, saturation, and brightness grading with source-exact defaults\n\n## Verified source material\n\n- `src/shaders/neuform-isolated/sources/kinetic-lathe-certificate.html`\n- `src/shaders/neuform-isolated/NeuformCraftEffects.tsx`\n\nSource revision: `SHA-256 dc48266f23df`\n\n## Implementation steps\n\n1. Open every verified source file listed above and identify the renderer, host lifecycle, styles, and assets before editing.\n2. Copy the complete canonical HTML source byte-for-byte so its shader strings, materials, DOM, timing, and initialization order remain auditable.\n3. After the source load event, retain only the `#cert` certificate; do not rewrite the renderer or approximate the composition.\n4. Force retained background targets to the sandbox viewport and center retained buttons without changing their internal pointer or shader state.\n5. Dispatch one resize event after reparenting so the exact source renderer recalculates its backing resolution.\n6. Apply optional hue, saturation, and brightness only to the outer iframe; omit the filter at 0/1/1 so source color remains exact.\n7. Keep the sandbox isolated with `allow-scripts` only; removing the iframe must release its document, listeners, frames, and graphics contexts together.\n8. Give the local component a sized, overflow-controlled parent and verify desktop, mobile, reduced-motion, and context-loss behavior.\n\nAsset handling: This effect has no required external assets.\n\n## Local component example\n\nImport the copied local component rather than a package entrypoint:\n\n```tsx\nimport { EngravedCertificate } from \"./effects/engraved-certificate/EngravedCertificate\";\nimport \"./effects/engraved-certificate/styles.css\";\n\nexport function Scene() {\n return
;\n}\n```\n\n## Core renderer pattern\n\nThis excerpt documents orchestration only. Copy the exact shader, geometry, pass, and interaction code from the verified source files.\n\n```tsx\nconst focusedSource = canonicalHtml\n .replace(\"\", focusStyles + \"\")\n .replace(\"\", focusAfterLoadScript + \"\");\n\nreturn