From f9ae82aa93782e47e178d60beb69da7b671f7be3 Mon Sep 17 00:00:00 2001 From: rohan Date: Wed, 12 Aug 2026 20:32:14 +0530 Subject: [PATCH 1/2] wip: swiss redesign Signed-off-by: rohan --- .phase.json | 8 + package.json | 1 + src/components/AboutPhase.jsx | 4 +- src/components/Button.jsx | 27 +- src/components/Card.jsx | 82 +----- src/components/Code.jsx | 39 +-- src/components/CopyButton.jsx | 6 +- src/components/DarkVeil.jsx | 350 ++++++++++++++++++++++++ src/components/Deployments.jsx | 21 +- src/components/DocActions.jsx | 12 +- src/components/Footer.jsx | 81 +++--- src/components/GridPattern.jsx | 4 + src/components/Guides.jsx | 54 ---- src/components/Header.jsx | 53 ++-- src/components/Heading.jsx | 8 +- src/components/HeroPattern.jsx | 51 ++-- src/components/Layout.jsx | 16 +- src/components/MobileNavigation.jsx | 8 +- src/components/ModeToggle.jsx | 4 +- src/components/Navigation.jsx | 37 +-- src/components/Platform.jsx | 6 +- src/components/PlatformsIntegration.jsx | 23 +- src/components/Search.jsx | 46 ++-- src/components/SecurityCards.jsx | 2 +- src/components/SecurityResources.jsx | 2 +- src/components/TabGroup.jsx | 9 +- src/components/Tag.jsx | 26 +- src/components/mdx.jsx | 34 +-- src/pages/_app.jsx | 23 +- src/pages/_document.jsx | 4 +- src/pages/index.mdx | 14 +- src/styles/tailwind.css | 65 ++++- tailwind.config.js | 8 + typography.js | 195 +++++++------ yarn.lock | 5 + 35 files changed, 845 insertions(+), 483 deletions(-) create mode 100644 .phase.json create mode 100644 src/components/DarkVeil.jsx delete mode 100644 src/components/Guides.jsx diff --git a/.phase.json b/.phase.json new file mode 100644 index 00000000..510884e3 --- /dev/null +++ b/.phase.json @@ -0,0 +1,8 @@ +{ + "version": "2", + "phaseApp": "Satcom", + "appId": "b9fb7a0d-534f-4e46-aa0f-c619a651477e", + "defaultEnv": "Development", + "envId": "88e733de-3126-4602-8089-5fd019a2b798", + "monorepoSupport": false +} \ No newline at end of file diff --git a/package.json b/package.json index 59f439d1..76207331 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "next": "14.2.1", "next-seo": "^6.0.0", "next-sitemap": "^4.2.3", + "ogl": "^1.0.11", "postcss": "^8.4.31", "postcss-focus-visible": "^6.0.4", "posthog-js": "^1.392.0", diff --git a/src/components/AboutPhase.jsx b/src/components/AboutPhase.jsx index fda49402..46729778 100644 --- a/src/components/AboutPhase.jsx +++ b/src/components/AboutPhase.jsx @@ -36,10 +36,10 @@ export function About() { About Phase -
+
{resources.map((resource) => (
-

+

{resource.name}

diff --git a/src/components/Button.jsx b/src/components/Button.jsx index 6319920f..f0fcb27b 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -14,16 +14,18 @@ function ArrowIcon(props) { ) } +// The emerald CTA pill is identical in both modes (no dark: variants needed). +const emeraldPill = + 'gap-2 whitespace-nowrap rounded-full bg-emerald-500 px-4 py-2 text-[13px] font-medium text-zinc-950 hover:bg-emerald-400 hover:text-zinc-950 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-400' + const variantStyles = { - primary: - 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-1 dark:ring-inset dark:ring-emerald-400/20 dark:hover:bg-emerald-400/10 dark:hover:text-emerald-300 dark:hover:ring-emerald-300', + primary: emeraldPill, + filled: emeraldPill, secondary: - 'rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300', - filled: - 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-500 dark:text-white dark:hover:bg-emerald-400', + 'gap-2 whitespace-nowrap rounded-full bg-zinc-100 px-4 py-2 font-mono text-xs uppercase tracking-[0.08em] text-zinc-700 ring-1 ring-inset ring-zinc-200 hover:bg-zinc-200 hover:text-zinc-900 focus-visible:outline focus-visible:outline-1 focus-visible:outline-zinc-400 dark:bg-zinc-900 dark:text-zinc-300 dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-100', outline: - 'rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white', - text: 'text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-500', + 'gap-2 whitespace-nowrap rounded-full px-4 py-2 font-mono text-xs uppercase tracking-[0.08em] text-zinc-600 ring-1 ring-inset ring-zinc-300 hover:bg-zinc-50 hover:text-zinc-900 hover:ring-zinc-400 focus-visible:outline focus-visible:outline-1 focus-visible:outline-zinc-400 dark:text-zinc-300 dark:ring-zinc-700 dark:hover:bg-zinc-900/40 dark:hover:text-zinc-100 dark:hover:ring-zinc-500', + text: 'gap-2 font-mono text-[11px] uppercase tracking-[0.12em] text-emerald-600 hover:text-emerald-700 focus-visible:outline focus-visible:outline-1 focus-visible:outline-zinc-400 dark:text-emerald-400 dark:hover:text-emerald-300', } export function Button({ @@ -36,7 +38,7 @@ export function Button({ let Component = props.href ? Link : 'button' className = clsx( - 'inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition', + 'group inline-flex w-fit items-center justify-center transition-colors duration-150', variantStyles[variant], className ) @@ -44,10 +46,11 @@ export function Button({ let arrowIcon = ( ) diff --git a/src/components/Card.jsx b/src/components/Card.jsx index 4f94e91b..eb003145 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -1,70 +1,14 @@ -import { motion, useMotionTemplate, useMotionValue } from 'framer-motion' -import { GridPattern } from '@/components/GridPattern' - -function CardPattern({ mouseX, mouseY, ...gridProps }) { - let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)` - let style = { maskImage, WebkitMaskImage: maskImage } - - return ( -

-
- +// Flat swiss hairline cell. `pattern` is accepted-and-ignored so legacy +// consumers (which passed spotlight grid coordinates) compile untouched. +export function Card({ index, pattern, children }) { + return ( +
+ {index && ( +
+ {index}
- - - - -
- ) - } - - export function Card({ pattern, children }) { - let mouseX = useMotionValue(0) - let mouseY = useMotionValue(0) - - function onMouseMove({ currentTarget, clientX, clientY }) { - let { left, top } = currentTarget.getBoundingClientRect() - mouseX.set(clientX - left) - mouseY.set(clientY - top) - } - - const defaultPattern = { - y: -6, - squares: [ - [-1, 2], - [1, 3], - ], - } - - return ( -
- -
-
- {children} -
-
- ) - } - \ No newline at end of file + )} + {children} +
+ ) +} diff --git a/src/components/Code.jsx b/src/components/Code.jsx index c3b8582d..3ad72799 100644 --- a/src/components/Code.jsx +++ b/src/components/Code.jsx @@ -51,17 +51,15 @@ function CodePanelHeader({ tag, label }) { } return ( -
+
{tag && ( -
+
{tag}
)} - {tag && label && ( - - )} + {tag && label && } {label && ( - {label} + {label} )}
) @@ -71,17 +69,19 @@ function CodePanel({ tag, label, code, children }) { let child = Children.only(children) return ( -
+
-
{children}
-
- +
+          {children}
+        
+
+
- + Copy
@@ -99,21 +99,21 @@ function CodeGroupHeader({ title, children, selectedIndex }) { } return ( -
+
{title && ( -

+
{title} -

+
)} {hasTabs && ( - + {Children.map(children, (child, childIndex) => ( {getPanelTitle(child.props)} @@ -223,7 +223,8 @@ export function CodeGroup({ children, title, ...props }) { {children} diff --git a/src/components/CopyButton.jsx b/src/components/CopyButton.jsx index d6a130d9..ad3310dc 100644 --- a/src/components/CopyButton.jsx +++ b/src/components/CopyButton.jsx @@ -9,6 +9,7 @@ export const CopyButton = ({ buttonVariant, title, onCopy, + className, }) => { const [copyCount, setCopyCount] = useState(0) const copied = copyCount > 0 @@ -41,6 +42,7 @@ export const CopyButton = ({ variant={variant} title={title || 'Copy to clipboard'} onClick={handleClick} + className={className} >
{children || (
- + Copy
)} @@ -60,7 +62,7 @@ export const CopyButton = ({ diff --git a/src/components/DarkVeil.jsx b/src/components/DarkVeil.jsx new file mode 100644 index 00000000..bdb02864 --- /dev/null +++ b/src/components/DarkVeil.jsx @@ -0,0 +1,350 @@ +import { useEffect, useRef } from 'react' + +/** + * DarkVeil — ported from the phase.dev website (JSX conversion of + * src/components/redesign/swiss/backgrounds/DarkVeil.tsx there; originally + * adapted from the MIT-licensed React Bits component), using the `ogl` WebGL + * renderer. Keep the two in sync when tuning. + * + * - `ogl` is imported lazily inside the effect so the module never evaluates + * during SSR / static export; the canvas renders identical markup either way. + * - The rAF loop is IntersectionObserver-gated (off-screen veils cost + * nothing), a warm first frame paints at load, phones get a dpr cap, and + * prefers-reduced-motion renders a single static frame. + */ + +const vertex = ` +attribute vec2 position; +void main(){gl_Position=vec4(position,0.0,1.0);} +` + +const fragment = ` +// The CPPN carries very large weights (|w| up to ~500) and exp() in sigmoid, +// so it needs highp — lowp/mediump overflow to garbage and each driver clamps +// differently (the "diagonal cut-off" on some GPUs/ANGLE builds). +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +uniform vec2 uResolution; +uniform float uTime; +uniform float uHueShift; +uniform float uNoise; +uniform float uScan; +uniform float uScanFreq; +uniform float uWarp; +uniform float uEmerald; +uniform float uCover; +uniform float uZoom; +uniform vec3 uC0; +uniform vec3 uC1; +uniform vec3 uC2; +#define iTime uTime +#define iResolution uResolution + +vec4 buf[8]; +float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);} + +mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312); +mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703); + +vec3 hueShiftRGB(vec3 col,float deg){ + vec3 yiq=rgb2yiq*col; + float rad=radians(deg); + float cosh=cos(rad),sinh=sin(rad); + vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh); + return clamp(yiq2rgb*yiqShift,0.0,1.0); +} + +// Tint ramp: dark -> mid -> bright, driven by uniform stops. Fed the animated +// value so the veil stays on-palette instead of the CPPN's native rainbow. +vec3 colorRamp(float t){ + vec3 lo=mix(uC0,uC1,smoothstep(0.0,0.5,t)); + return mix(lo,uC2,smoothstep(0.45,1.0,t)); +} + +vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));} + +vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){ + buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1); + buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.); + buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676); + buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668); + buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]); + buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186); + buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718); + buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]); + buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112); + buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503); + buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]); + buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386); + buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676); + buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]); + buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.); + buf[0]=sigmoid(buf[0]); + return vec4(buf[0].x,buf[0].y,buf[0].z,1.); +} + +void mainImage(out vec4 fragColor,in vec2 fragCoord){ + vec2 uv=fragCoord/uResolution.xy*2.-1.; + float aspect=uResolution.x/uResolution.y; + // Contain: fit the pattern to height. Cover (wide/short strips): fill the + // wider dimension so the flow spans the strip instead of collapsing to a + // small central blob. + if(uCover>0.5){ + if(aspect>1.0) uv.y/=aspect; else uv.x*=aspect; + } else { + uv.x*=aspect; + } + uv.y*=-1.; + // uZoom<1 samples a smaller window of the CPPN (less of the bright core in + // frame) → a smaller/tighter glow; >1 spreads it wider. + uv*=uZoom; + uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05; + fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime)); +} + +void main(){ + vec4 col;mainImage(col,gl_FragCoord.xy); + col.rgb=hueShiftRGB(col.rgb,uHueShift); + // Recolour to the palette from the animated value (max channel keeps + // bright flow bright regardless of the CPPN's native hue), with a mild + // curve for depth so the canvas stays mostly dark with emerald pools. + float v=max(col.r,max(col.g,col.b)); + // Sub-1 gamma widens the veil's shape/spread so the recessed phase of the + // drift cycle stays fuller. v=0 still maps to the near-black floor, so the + // dark gaps and contrast are preserved (brightness floor unchanged). + v=pow(clamp(v,0.0,1.0),0.62); + col.rgb=mix(col.rgb,colorRamp(v),uEmerald); + float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5; + col.rgb*=1.-(scanline_val*scanline_val)*uScan; + col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise; + gl_FragColor=vec4(clamp(col.rgb,0.0,1.0),1.0); +} +` + +/** Ramp stops (dark -> mid -> bright), 0-1 RGB, fed to the tint shader. */ +export const VEIL_PALETTES = { + emerald: [ + [0.012, 0.045, 0.033], + [0.043, 0.325, 0.216], + [0.29, 0.937, 0.612], + ], +} + +export default function DarkVeil({ + hueShift = 0, + noiseIntensity = 0, + scanlineIntensity = 0, + speed = 0.5, + scanlineFrequency = 0, + warpAmount = 0, + resolutionScale = 1, + emerald = 1, + palette = 'emerald', + cover = false, + zoom = 1, + className, +}) { + const ref = useRef(null) + const renderRef = useRef(undefined) + + // Live tunables read by the render loop each frame, so changing speed / + // palette / etc. updates uniforms in place without tearing down the WebGL + // context (only resolutionScale, which sets dpr, forces a rebuild). + const live = useRef({}) + live.current = { + hueShift, + noiseIntensity, + scanlineIntensity, + speed, + scanlineFrequency, + warpAmount, + emerald, + palette, + cover, + zoom, + } + + useEffect(() => { + const canvas = ref.current + const parent = canvas?.parentElement + if (!canvas || !parent) return undefined + + let disposed = false + let frame = 0 + let cleanupResize + let loseContext + let visibilityObserver + + // Lazy import keeps `ogl` out of the SSR bundle so the module never + // evaluates on the server; the markup is identical either way. + import('ogl') + .then(({ Renderer, Program, Mesh, Triangle, Vec2 }) => { + if (disposed) return + + // resolutionScale reduces the drawing-buffer resolution (perf) via dpr + // — NOT the display size. Phones get a tighter dpr cap: the veil is a + // soft organic glow, so the resolution loss is invisible while the + // CPPN's per-fragment cost drops by ~40%. + const phone = window.matchMedia( + '(pointer: coarse) and (max-width: 820px)' + ).matches + const renderer = new Renderer({ + dpr: + Math.min(window.devicePixelRatio, phone ? 1.5 : 2) * + resolutionScale, + canvas, + }) + const { gl } = renderer + const geometry = new Triangle(gl) + const program = new Program(gl, { + vertex, + fragment, + uniforms: { + uTime: { value: 0 }, + uResolution: { value: new Vec2() }, + uHueShift: { value: hueShift }, + uNoise: { value: noiseIntensity }, + uScan: { value: scanlineIntensity }, + uScanFreq: { value: scanlineFrequency }, + uWarp: { value: warpAmount }, + uEmerald: { value: emerald }, + uCover: { value: cover ? 1 : 0 }, + uZoom: { value: zoom }, + uC0: { value: VEIL_PALETTES[palette][0] }, + uC1: { value: VEIL_PALETTES[palette][1] }, + uC2: { value: VEIL_PALETTES[palette][2] }, + }, + }) + const mesh = new Mesh(gl, { geometry, program }) + + // Whether the rAF loop is currently driving frames, and the last time + // value handed to render() — resize() repaints from these when no + // loop is running. + let running = false + let lastT = 0 + + // Pull every tunable from the live ref each frame so prop changes + // apply without rebuilding the context. + const render = (t) => { + lastT = t + const p = live.current + const u = program.uniforms + u.uTime.value = (t / 1000) * p.speed + u.uHueShift.value = p.hueShift + u.uNoise.value = p.noiseIntensity + u.uScan.value = p.scanlineIntensity + u.uScanFreq.value = p.scanlineFrequency + u.uWarp.value = p.warpAmount + u.uEmerald.value = p.emerald + u.uCover.value = p.cover ? 1 : 0 + u.uZoom.value = p.zoom + const stops = VEIL_PALETTES[p.palette] + ;[u.uC0.value, u.uC1.value, u.uC2.value] = stops + renderer.render({ scene: mesh }) + } + renderRef.current = render + + const resize = () => { + const w = parent.clientWidth + const h = parent.clientHeight + renderer.setSize(w, h) + // gl_FragCoord is in drawing-buffer (device) pixels, so uResolution + // must be the buffer size — not CSS pixels — or the pattern balloons + // on high-DPR displays. + program.uniforms.uResolution.value.set( + gl.drawingBufferWidth, + gl.drawingBufferHeight + ) + // ogl's setSize resets the drawing buffer to blank. When a loop is + // running the next tick repaints; otherwise repaint the standby + // frame here or the veil would stay blank until its next crossing. + if (!running) render(lastT) + } + const observer = new ResizeObserver(resize) + observer.observe(parent) + // Initial size + warm first frame: compiles the pipeline at load (off + // the scroll path) and leaves a complete frame on the canvas for the + // moment it scrolls into view. + resize() + cleanupResize = () => observer.disconnect() + loseContext = () => gl.getExtension('WEBGL_lose_context')?.loseContext() + + const reduce = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches + if (reduce) { + // Static, complete single frame — no animation loop. + render(4200) + return + } + + // The CPPN fragment shader is expensive enough to eat a weak mobile + // GPU's whole frame budget, so the loop only runs while the veil is + // actually near the viewport; the absolute clock keeps the drift + // continuous across pauses. + const start = performance.now() + const loop = () => { + if (!running) return + render(performance.now() - start) + frame = requestAnimationFrame(loop) + } + visibilityObserver = new IntersectionObserver( + (entries) => { + if (disposed) return + // Deliveries coalesce under jank — act on the NEWEST record, or + // an out-and-back-in crossing lands on the stale state. + const shouldRun = entries[entries.length - 1]?.isIntersecting ?? true + if (shouldRun && !running) { + running = true + frame = requestAnimationFrame(loop) + } else if (!shouldRun && running) { + running = false + cancelAnimationFrame(frame) + } + }, + { rootMargin: '200px 0px' } + ) + visibilityObserver.observe(parent) + }) + .catch(() => { + /* WebGL unavailable — the page simply shows the bare canvas. */ + }) + + return () => { + disposed = true + cancelAnimationFrame(frame) + visibilityObserver?.disconnect() + renderRef.current = undefined + cleanupResize?.() + loseContext?.() + } + // Only resolutionScale (dpr) requires a rebuild; all other props update + // live through `live.current` inside render(). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resolutionScale]) + + // Under reduced motion there is no loop, so re-render the static frame when + // a tunable changes. + useEffect(() => { + const reduce = + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches + if (reduce) renderRef.current?.(4200) + }, [ + hueShift, + noiseIntensity, + scanlineIntensity, + speed, + scanlineFrequency, + warpAmount, + emerald, + palette, + cover, + zoom, + ]) + + return