diff --git a/landing/app/components/ShaderWordmark.tsx b/landing/app/components/ShaderWordmark.tsx new file mode 100644 index 0000000..e0bd6c0 --- /dev/null +++ b/landing/app/components/ShaderWordmark.tsx @@ -0,0 +1,303 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +/** + * The hero wordmark, rendered live on the GPU. + * + * The

stays in the DOM exactly as before (SEO, a11y, layout, and + * the fallback for browsers without WebGPU). When vgpu comes up we + * rasterize the same text with the same font into a mask texture, + * hand it to the shader, and make the DOM glyphs transparent so the + * canvas shows through in their place. + * + * - WebGPU missing / init fails / device lost -> plain text, no canvas. + * - prefers-reduced-motion -> one static frame, no loop. + * - Off-screen or hidden tab -> loop paused. + * - Theme toggle -> re-tinted on the next frame. + */ +export function ShaderWordmark({ text = 'flicky' }: { text?: string }) { + const wrapRef = useRef(null); + const h1Ref = useRef(null); + const canvasRef = useRef(null); + const [live, setLive] = useState(false); + + useEffect(() => { + const wrap = wrapRef.current; + const h1 = h1Ref.current; + const canvas = canvasRef.current; + if (!wrap || !h1 || !canvas) return; + if (typeof navigator === 'undefined' || !('gpu' in navigator)) return; + + let cancelled = false; + const cleanups: Array<() => void> = []; + + (async () => { + await document.fonts.ready; + if (cancelled) return; + + const [vgpu, shader] = await Promise.all([ + import('vgpu'), + import('./wordmark.wgsl'), + ]); + if (cancelled) return; + + let gpu: Awaited>; + try { + gpu = await vgpu.init(); + } catch { + return; // no adapter — leave the DOM text alone + } + if (cancelled) { + gpu.dispose(); + return; + } + cleanups.push(() => gpu.dispose()); + + const reduceMq = window.matchMedia('(prefers-reduced-motion: reduce)'); + const isDark = () => document.documentElement.getAttribute('data-theme') === 'dark'; + + // --- pad the canvas so the halo has room outside the glyph box + const fontPx = () => parseFloat(getComputedStyle(h1).fontSize) || 100; + const applyPad = () => wrap.style.setProperty('--wm-pad', `${Math.round(fontPx() * 0.4)}px`); + applyPad(); + + const surf = vgpu.surface(gpu, canvas, { dpr: [1, 2], alphaMode: 'premultiplied', label: 'wordmark' }); + cleanups.push(() => surf.dispose()); + + const linear = vgpu.sampler(gpu, { + magFilter: 'linear', + minFilter: 'linear', + addressModeU: 'clamp-to-edge', + addressModeV: 'clamp-to-edge', + }); + + // --- rasterize the DOM text into an R (sharp) / G (blurred) mask + let maskTex: ReturnType | null = null; + const buildMask = (width: number, height: number, dpr: number) => { + const cs = getComputedStyle(h1); + const font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`; + const pad = fontPx() * 0.4; + + const draw = (ctx: CanvasRenderingContext2D, blur: number) => { + ctx.save(); + ctx.scale(dpr, dpr); + ctx.font = font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + if ('letterSpacing' in ctx) (ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = cs.letterSpacing; + if (blur > 0) ctx.filter = `blur(${blur}px)`; + const cssW = width / dpr; + const cssH = height / dpr; + // canvas is the h1 box plus pad on every side; center the run + // in the h1 box. letter-spacing adds trailing space after the + // last glyph, so nudge left by half of it. + const ls = parseFloat(cs.letterSpacing) || 0; + ctx.fillText(text, cssW / 2 + ls / 2, cssH / 2 + pad * 0.02); + ctx.restore(); + }; + + const sharp = document.createElement('canvas'); + sharp.width = width; + sharp.height = height; + const sc = sharp.getContext('2d')!; + sc.fillStyle = '#000'; + sc.fillRect(0, 0, width, height); + sc.fillStyle = '#f00'; + draw(sc, 0); + + const glow = document.createElement('canvas'); + glow.width = width; + glow.height = height; + const gc = glow.getContext('2d')!; + gc.fillStyle = '#0f0'; + draw(gc, fontPx() * 0.16); + + sc.globalCompositeOperation = 'lighter'; + sc.drawImage(glow, 0, 0); + + const next = vgpu.texture(gpu, { + kind: '2d', + size: [width, height], + format: 'rgba8unorm', + usage: ['texture_binding', 'copy_dst', 'render_attachment'], // copyExternalImageToTexture requires render_attachment + label: 'wordmark-mask', + }); + gpu.gpu.queue.copyExternalImageToTexture( + { source: sharp }, + { texture: next.gpu }, + [width, height], + ); + const prev = maskTex; + maskTex = next; + return { next, prev }; + }; + + const fx = vgpu.effect(gpu, shader.default, { + label: 'wordmark', + set: { + params: { + time: 0, + theme: isDark() ? 1 : 0, + aspect: 1, + reveal: reduceMq.matches ? 1 : 0, + pointer: [-10, -10], + texel: surf.texelSize, + pointerStrength: 0, + motion: reduceMq.matches ? 0 : 1, + }, + samp: linear, + }, + }); + + // --- state driven from DOM events, consumed per frame + let pointer: [number, number] = [-10, -10]; + let pointerTarget = 0; + let pointerStrength = 0; + let reveal = reduceMq.matches ? 1 : 0; + let revealStart = -1; + let visible = true; + let dirty = true; // something changed that a static render must pick up + let loopHandle: { stop(): void } | null = null; + let firstFrameShown = false; + const clk = vgpu.clock(gpu); + + const renderOnce = () => { + if (cancelled || !maskTex) return; + vgpu.frame(gpu, (f) => f.pass(surf, fx)); + if (!firstFrameShown) { + firstFrameShown = true; + setLive(true); + } + }; + + const tick = () => { + if (cancelled || !maskTex) return; + const now = performance.now(); + if (revealStart < 0) revealStart = now; + if (reveal < 1) { + const u = Math.min(1, (now - revealStart) / 1500); + reveal = 1 - Math.pow(1 - u, 3); + } + pointerStrength += (pointerTarget - pointerStrength) * 0.08; + fx.set({ + params: { + time: clk.time, + reveal, + pointer, + pointerStrength, + theme: isDark() ? 1 : 0, + }, + }); + }; + + const startLoop = () => { + if (loopHandle || reduceMq.matches || !visible || document.hidden) return; + loopHandle = vgpu.frameLoop( + gpu, + (f) => { + tick(); + f.pass(surf, fx); + if (!firstFrameShown) { + firstFrameShown = true; + setLive(true); + } + }, + { fps: 60 }, + ); + }; + const stopLoop = () => { + loopHandle?.stop(); + loopHandle = null; + }; + cleanups.push(stopLoop); + + const refresh = () => { + if (reduceMq.matches) { + fx.set({ params: { theme: isDark() ? 1 : 0, reveal: 1, motion: 0 } }); + renderOnce(); + } else { + stopLoop(); + startLoop(); + } + }; + + // --- size: the surface auto-resizes from layout; rebuild the mask to match + const offResize = surf.onResize(({ width, height, dpr }) => { + applyPad(); + const { prev } = buildMask(width, height, dpr); + fx.set({ + params: { aspect: width / height, texel: surf.texelSize }, + mask: maskTex!, + }); + prev?.destroy(); + dirty = true; + // resize callbacks may not open a frame; defer the static render + if (reduceMq.matches) queueMicrotask(() => { if (dirty) { dirty = false; renderOnce(); } }); + }); + cleanups.push(offResize); + cleanups.push(() => maskTex?.destroy()); + + // --- pointer: ripples follow the cursor anywhere over the hero stage + const stage = (wrap.closest('.stage') as HTMLElement | null) ?? wrap; + const onMove = (e: PointerEvent) => { + const r = canvas.getBoundingClientRect(); + pointer = [(e.clientX - r.left) / r.width, (e.clientY - r.top) / r.height]; + pointerTarget = 1; + }; + const onLeave = () => { pointerTarget = 0; }; + stage.addEventListener('pointermove', onMove, { passive: true }); + stage.addEventListener('pointerleave', onLeave, { passive: true }); + cleanups.push(() => { + stage.removeEventListener('pointermove', onMove); + stage.removeEventListener('pointerleave', onLeave); + }); + + // --- pause when off-screen or the tab is hidden + const io = new IntersectionObserver(([entry]) => { + visible = !!entry?.isIntersecting; + if (visible) startLoop(); else stopLoop(); + }, { threshold: 0.01 }); + io.observe(canvas); + cleanups.push(() => io.disconnect()); + const onVis = () => { if (document.hidden) stopLoop(); else startLoop(); }; + document.addEventListener('visibilitychange', onVis); + cleanups.push(() => document.removeEventListener('visibilitychange', onVis)); + + // --- theme + reduced motion changes + const mo = new MutationObserver(refresh); + mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + cleanups.push(() => mo.disconnect()); + reduceMq.addEventListener('change', refresh); + cleanups.push(() => reduceMq.removeEventListener('change', refresh)); + + // --- if the device dies, fall back to the DOM text + const offErr = gpu.onError(() => { + setLive(false); + stopLoop(); + }); + cleanups.push(offErr); + + // go + if (reduceMq.matches) renderOnce(); else startLoop(); + })().catch(() => { + // any unexpected failure: keep the plain wordmark + setLive(false); + }); + + return () => { + cancelled = true; + setLive(false); + for (const fn of cleanups.splice(0).reverse()) { + try { fn(); } catch { /* teardown is best-effort */ } + } + }; + }, [text]); + + return ( +
+

{text}

+
+ ); +} diff --git a/landing/app/components/wordmark.wgsl b/landing/app/components/wordmark.wgsl new file mode 100644 index 0000000..6c1fe87 --- /dev/null +++ b/landing/app/components/wordmark.wgsl @@ -0,0 +1,112 @@ +// flicky wordmark — the hero headline as a live WebGPU surface. +// +// The glyphs come in as a mask texture rasterized by the DOM font +// (R = sharp coverage, G = blurred coverage for the halo). Inside the +// letters we pour a slow, domain-warped simplex "liquid" in the brand +// blues, overlay the desktop's dotted wallpaper grid, and sweep a thin +// glint across every few seconds. The pointer pushes ripples through +// the liquid, and the whole thing reveals left-to-right on load. + +import { fbmSimplex3d, simplex3d } from "@vgpu/wgsl-std/noise/simplex"; + +struct Params { + time: f32, + theme: f32, // 0 = light, 1 = dark + aspect: f32, // canvas width / height + reveal: f32, // 0..1 intro wipe + pointer: vec2f, // pointer in uv space (far away when absent) + texel: vec2f, // 1 / canvas size + pointerStrength: f32, // eased 0..1 + motion: f32, // 0 = reduced motion (freeze), 1 = animate +} + +@group(0) @binding(0) var params: Params; +@group(0) @binding(1) var mask: texture_2d; +@group(0) @binding(2) var samp: sampler; + +fn palette(t: f32, theme: f32) -> vec3f { + // light: deep royal -> flicky blue -> sky -> near-white highlight + let l0 = vec3f(0.114, 0.306, 0.847); // #1d4ed8 + let l1 = vec3f(0.145, 0.388, 0.922); // #2563eb + let l2 = vec3f(0.220, 0.741, 0.973); // #38bdf8 + let l3 = vec3f(0.859, 0.918, 0.996); // #dbeafe + // dark: brighter, cooler, more cyan + let d0 = vec3f(0.180, 0.420, 0.930); + let d1 = vec3f(0.357, 0.608, 1.000); // #5b9bff + let d2 = vec3f(0.404, 0.910, 0.976); // #67e8f9 + let d3 = vec3f(0.930, 0.980, 1.000); + + let c0 = mix(l0, d0, theme); + let c1 = mix(l1, d1, theme); + let c2 = mix(l2, d2, theme); + let c3 = mix(l3, d3, theme); + + let a = smoothstep(0.00, 0.45, t); + let b = smoothstep(0.40, 0.82, t); + let c = smoothstep(0.84, 1.00, t) * 0.75; // keep highlights from blowing out to white + return mix(mix(mix(c0, c1, a), c2, b), c3, c); +} + +@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f { + let t = params.time * params.motion; + let m = textureSample(mask, samp, uv); + let ink = m.r; + let halo = m.g; + + // early out for pixels that touch neither the glyphs nor the halo + if (ink < 0.002 && halo < 0.002) { + return vec4f(0.0); + } + + // aspect-correct coordinates, y in [0, 1] + let p = vec2f(uv.x * params.aspect, uv.y); + + // --- liquid: low-frequency warp, mid-frequency detail, bounded time + let z = sin(t * 0.11) * 3.0 + cos(t * 0.07) * 2.0; + let drift = p * 1.6 + vec2f(t * 0.05, -t * 0.02); + let warp = vec2f( + fbmSimplex3d(vec3f(drift, z), 3, 2.0, 0.5), + fbmSimplex3d(vec3f(drift + vec2f(41.0, -23.0), z + 17.0), 3, 2.0, 0.5), + ); + var field = fbmSimplex3d(vec3f(p * 2.2 + warp * 0.6, z * 0.5 + 9.0), 3, 2.17, 0.5); + + // --- pointer ripple: rings fanning out from the cursor, fading with distance + let pp = vec2f(params.pointer.x * params.aspect, params.pointer.y); + let d = distance(p, pp); + let ripple = sin(d * 34.0 - t * 5.5) * exp(-d * 3.2) * params.pointerStrength; + field = field + ripple * 0.35; + + // --- shape the field into a palette position, biased toward the mid blues + let tone = clamp((field + 0.55) / 1.1, 0.0, 1.0); + var col = palette(tone, params.theme); + + // vertical sheen so the letters read as glossy, not flat + let sheen = smoothstep(0.0, 1.0, 1.0 - uv.y) * 0.10; + col = col + sheen * mix(vec3f(0.6, 0.8, 1.0), vec3f(0.7, 0.95, 1.0), params.theme); + + // --- the desktop's dotted wallpaper, printed through the glyphs + let cell = fract(p * 22.0) - 0.5; + let dot = 1.0 - smoothstep(0.10, 0.16, length(cell)); + col = col * (1.0 - dot * mix(0.10, 0.15, params.theme)); + + // --- glint: a thin diagonal streak that sweeps across every ~7s + let phase = fract(t * 0.14); + let axis = (p.x * 0.72 + uv.y * 0.28) / max(params.aspect, 0.001); + let streak = 1.0 - smoothstep(0.0, 0.035, abs(axis - (phase * 1.6 - 0.3))); + let glintOn = smoothstep(0.0, 0.08, phase) * (1.0 - smoothstep(0.92, 1.0, phase)); + col = col + streak * glintOn * 0.55 * vec3f(1.0); + + // --- intro reveal: a noisy left-to-right wipe + let edge = simplex3d(vec3f(p * 5.0, 3.0)) * 0.08; + let wipe = smoothstep(params.reveal * 1.25 - 0.25 + edge, params.reveal * 1.25 - 0.05 + edge, uv.x); + let show = 1.0 - wipe; + + // --- halo outside the letters, tinted by the same field + let haloCol = mix(vec3f(0.36, 0.60, 1.0), vec3f(0.42, 0.72, 1.0), params.theme); + let haloA = halo * (1.0 - ink) * mix(0.28, 0.55, params.theme) * (0.75 + 0.25 * field) * show; + + let inkA = ink * show; + let rgb = col * inkA + haloCol * haloA; + let a = inkA + haloA; + return vec4f(rgb, a); // premultiplied +} diff --git a/landing/app/globals.css b/landing/app/globals.css index cfd064f..723cc10 100644 --- a/landing/app/globals.css +++ b/landing/app/globals.css @@ -434,6 +434,34 @@ main { line-height: 0.95; } +/* the wordmark as a WebGPU surface: the

keeps the layout, the + canvas sits on top with room for the halo, and once the first frame + is on screen the DOM glyphs go transparent so the shader shows through */ +.wordmark-wrap { + --wm-pad: 40px; + position: relative; + display: inline-block; +} +.wordmark-wrap .wordmark { + position: relative; + z-index: 1; + transition: color 0.25s ease; +} +.wordmark-gpu { + position: absolute; + left: calc(-1 * var(--wm-pad)); + top: calc(-1 * var(--wm-pad)); + width: calc(100% + 2 * var(--wm-pad)); + height: calc(100% + 2 * var(--wm-pad)); + display: block; + pointer-events: none; + z-index: 2; + opacity: 0; + transition: opacity 0.35s ease; +} +.wordmark-wrap.is-gpu .wordmark { color: transparent; } +.wordmark-wrap.is-gpu .wordmark-gpu { opacity: 1; } + .lead { margin-top: 16px; font-size: 20px; diff --git a/landing/app/page.tsx b/landing/app/page.tsx index e3d36fb..89ebd43 100644 --- a/landing/app/page.tsx +++ b/landing/app/page.tsx @@ -1,5 +1,6 @@ import { Mark } from './components/Mark'; import { HeroVideo } from './components/HeroVideo'; +import { ShaderWordmark } from './components/ShaderWordmark'; import { Win } from './components/Win'; import { Taskbar } from './components/Taskbar'; import { DesktopIcons } from './components/DesktopIcons'; @@ -147,7 +148,7 @@ export default function Page() {

windows · mac · linux

-

flicky

+

an ai buddy that lives on your desktop.

diff --git a/landing/bun.lock b/landing/bun.lock index 62e73de..c21b28d 100644 --- a/landing/bun.lock +++ b/landing/bun.lock @@ -8,6 +8,7 @@ "next": "^16.2.3", "react": "^19.2.5", "react-dom": "^19.2.5", + "vgpu": "^0.5.0", }, "devDependencies": { "@types/node": "22.10.5", @@ -70,6 +71,10 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="], + + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="], + "@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="], @@ -96,6 +101,18 @@ "@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="], + "@vgpu/adapter-mock": ["@vgpu/adapter-mock@0.5.0", "", { "dependencies": { "@vgpu/core": "0.5.0" } }, "sha512-YtgXOFn1qI+8oTJenG2wWbQPA28V0CfE5UJs5Xqbt6qyR/xaZ5LHH2XWTwFcjnqn2b1xQCyTatJCMyyOdweUnw=="], + + "@vgpu/adapter-node": ["@vgpu/adapter-node@0.5.0", "", { "dependencies": { "@vgpu/core": "0.5.0", "webgpu": "0.4.0" } }, "sha512-lgzqFcLDejwRgwlaNStzkwLNU9b4aN2OoPpWqT1UyPmK521BSw2OIQKzAuDIkzNlREla6KNC12G6bthhOzZKzw=="], + + "@vgpu/core": ["@vgpu/core@0.5.0", "", { "dependencies": { "@vgpu/wgsl": "0.5.0" } }, "sha512-z80DHRHViv6+FKKkPzggEtT1ZZs/WCKRQbzJuwYdA135DZaSbCMoRK4anhM+I/nOHuDpf7H3xjkwE6rfolWZUw=="], + + "@vgpu/wgsl": ["@vgpu/wgsl@0.5.0", "", { "dependencies": { "@vgpu/wgsl-std": "0.5.0" }, "peerDependencies": { "@vgpu/adapter-node": ">=0.1.6 <1.0.0 || ^0.5.0-rc.0" }, "optionalPeers": ["@vgpu/adapter-node"] }, "sha512-PDlDJem7J28C9bk+vPy2kYZEoW8+nYIx2oN4nu7/WF2V7ETx32BtmWcawwMbhiu/CVsobFafML0h0eqLRBNa3Q=="], + + "@vgpu/wgsl-std": ["@vgpu/wgsl-std@0.5.0", "", {}, "sha512-NjMyJxwy4evFNCSqFQy4EqAbZ7iU+NUyJYyG9tLUhbdDLzBmfZlk1S3PyRZ9olBFFfEiVW2JNVw10maPKLEs1A=="], + + "@webgpu/types": ["@webgpu/types@0.1.72", "", {}, "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": "dist/cli.cjs" }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="], "caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], @@ -104,14 +121,22 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "next": ["next@16.2.3", "", { "dependencies": { "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.3", "@next/swc-darwin-x64": "16.2.3", "@next/swc-linux-arm64-gnu": "16.2.3", "@next/swc-linux-arm64-musl": "16.2.3", "@next/swc-linux-x64-gnu": "16.2.3", "@next/swc-linux-x64-musl": "16.2.3", "@next/swc-win32-arm64-msvc": "16.2.3", "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "pixelmatch": ["pixelmatch@7.2.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg=="], + + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], @@ -133,5 +158,13 @@ "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "vgpu": ["vgpu@0.5.0", "", { "dependencies": { "@modelcontextprotocol/server": "2.0.0", "@vgpu/adapter-mock": "0.5.0", "@vgpu/adapter-node": "0.5.0", "@vgpu/core": "0.5.0", "@vgpu/wgsl": "0.5.0", "@vgpu/wgsl-std": "0.5.0", "@webgpu/types": "^0.1.69", "pixelmatch": "^7.2.0", "pngjs": "^7.0.0", "wgpu-matrix": "^3.4.2", "zod": "^4.4.3" }, "peerDependencies": { "three": ">=0.180.0 <0.200.0" }, "optionalPeers": ["three"], "bin": { "vgpu": "bin/vgpu.js" } }, "sha512-isQfMHRTysYFfT8UWTzqQI/ROw2L7lVkj7xoUJlvIF6FjYxBNS/YNG7qp3tZ7ZvZZ3uwYjso+geQv/bEq5Hl3w=="], + + "webgpu": ["webgpu@0.4.0", "", { "dependencies": { "@webgpu/types": "^0.1.69", "debug": "^4.4.0" } }, "sha512-F5pimn3Aoi0zWjuRdiVs5TnrUwSzD2lESBohsIUsqyitWkGRQlXU2fhV6ycXlQTa1bvAf3sjqiUpBEpmSQ5ptA=="], + + "wgpu-matrix": ["wgpu-matrix@3.4.2", "", {}, "sha512-IfZFbG7olEYBD76VCZy2TzqM57GKy3LT++AZh37tMHPPByO6R2h61zXGBZFNPi2IzxNivNi8y5dvze5PjaczjA=="], + + "zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], } } diff --git a/landing/next.config.mjs b/landing/next.config.mjs index 8967ad6..36916c0 100644 --- a/landing/next.config.mjs +++ b/landing/next.config.mjs @@ -10,8 +10,28 @@ const nextConfig = { output: 'export', images: { unoptimized: true }, // Pin Turbopack's root to this folder so Next 16 doesn't wander up - // the tree and pick up the outer Electron app's lockfile. - turbopack: { root: __dirname }, + // the tree and pick up the outer Electron app's lockfile. The .wgsl + // rule lets the hero wordmark shader import @vgpu/wgsl-std modules. + turbopack: { + root: __dirname, + rules: { + '*.wgsl': { + loaders: [{ loader: '@vgpu/wgsl/loader-webpack', options: { minify: true } }], + as: '*.js', + }, + }, + }, + // Same rule for anyone running the webpack bundler. + webpack(config) { + config.module ??= {}; + config.module.rules ??= []; + config.module.rules.push({ + test: /\.wgsl$/, + loader: '@vgpu/wgsl/loader-webpack', + options: { minify: true }, + }); + return config; + }, }; export default nextConfig; diff --git a/landing/package.json b/landing/package.json index 795c955..5c171d6 100644 --- a/landing/package.json +++ b/landing/package.json @@ -11,7 +11,8 @@ "dependencies": { "next": "^16.2.3", "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-dom": "^19.2.5", + "vgpu": "^0.5.0" }, "devDependencies": { "@types/node": "22.10.5",