@wave3d/core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/config/model.d.ts +104 -1
- package/dist/config/model.js +111 -1
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/presets.js +15 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +34 -0
- package/dist/renderer/WaveRenderer.js +232 -7
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/interaction.d.ts +119 -0
- package/dist/renderer/interaction.js +474 -0
- package/dist/renderer/interaction.js.map +1 -0
- package/dist/renderer/palette.js +14 -0
- package/dist/renderer/palette.js.map +1 -1
- package/dist/renderer/shaders.js +105 -0
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/shell/createWave.d.ts +9 -1
- package/dist/shell/createWave.js +7 -1
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/poster.d.ts +12 -0
- package/dist/shell/poster.js +4 -3
- package/dist/shell/poster.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +864 -219
- package/dist/standalone.d.ts +2 -2
- package/dist/standalone.js +2 -2
- package/dist/studio/StudioWaveRenderer.d.ts +15 -0
- package/dist/studio/StudioWaveRenderer.js +32 -1
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/index.d.ts +2 -1
- package/dist/studio/index.js +2 -1
- package/dist/studio/randomize.d.ts +3 -3
- package/dist/studio/randomize.js +4 -4
- package/dist/studio/randomize.js.map +1 -1
- package/dist/studio/thumbnail.d.ts +14 -0
- package/dist/studio/thumbnail.js +32 -0
- package/dist/studio/thumbnail.js.map +1 -0
- package/package.json +1 -1
- package/skills/wave3d/SKILL.md +20 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"palette.js","names":[],"sources":["../../src/renderer/palette.ts"],"sourcesContent":["import * as THREE from \"three\";\nimport { clamp01 } from \"../util/math\";\nimport { createDefaultMeshPoints } from \"../config/model\";\nimport type {\n BackgroundImageFit,\n BasicGradientType,\n ColorStop,\n MeshGradientPoint,\n} from \"../config/model\";\n\n/**\n * Bakes the gradient stops into a 2D palette texture, sampled in the shader as\n * `texture2D(u_paletteTexture, vec2(uv.x, uv.y))`. The texture is a real image, so\n * colour can vary independently along BOTH axes:\n * - X (uv.x, along the length): the gradient stops.\n * - Y (uv.y, across the width): an \"edge tint\" blended toward both long edges\n */\n\nconst TEX_W = 256; // resolution along the gradient (length)\nconst TEX_H = 64; // resolution across the width\n\nfunction smoothstep(value: number): number {\n return value * value * (3 - 2 * value);\n}\n\n/** \"#rrggbb\" → \"rgba(r,g,b,a)\" for canvas fills with alpha. */\nfunction hexToRgba(hex: string, alpha: number): string {\n const c = new THREE.Color(hex); // parses many formats; .r/.g/.b are linear…\n // …but we want the original sRGB bytes for the canvas, so re-encode.\n const srgb = c.clone().convertLinearToSRGB();\n const r = Math.round(srgb.r * 255);\n const g = Math.round(srgb.g * 255);\n const b = Math.round(srgb.b * 255);\n return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nexport interface PaletteTextureOptions {\n stops: ColorStop[];\n /** Cross-width edge tint colour (e.g. periwinkle). */\n edgeColor: string;\n /** 0 = flat 1-D gradient (no 2nd axis); higher = stronger cool edges. */\n edgeAmount: number;\n}\n\n/** Draw the palette into a 2D canvas. */\nexport function buildPaletteCanvas(opts: PaletteTextureOptions): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = TEX_W;\n canvas.height = TEX_H;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return canvas;\n\n // X axis: the main gradient (stops sorted by position).\n const stops = [...opts.stops].sort((a, b) => a.pos - b.pos);\n const grad = ctx.createLinearGradient(0, 0, TEX_W, 0);\n if (stops.length === 0) {\n grad.addColorStop(0, \"#ffffff\");\n } else {\n for (const s of stops) grad.addColorStop(clamp01(s.pos), s.color);\n }\n ctx.fillStyle = grad;\n ctx.fillRect(0, 0, TEX_W, TEX_H);\n\n // Y axis: blend the edge colour toward both long edges (V-shaped alpha), 0 in the\n // middle — a genuine second dimension of colour.\n const a = clamp01(opts.edgeAmount);\n if (a > 0.001) {\n const eg = ctx.createLinearGradient(0, 0, 0, TEX_H);\n eg.addColorStop(0, hexToRgba(opts.edgeColor, a));\n eg.addColorStop(0.5, hexToRgba(opts.edgeColor, 0));\n eg.addColorStop(1, hexToRgba(opts.edgeColor, a));\n ctx.fillStyle = eg;\n ctx.fillRect(0, 0, TEX_W, TEX_H);\n }\n\n return canvas;\n}\n\n/** A small string that changes whenever the texture would need rebuilding. */\nexport function paletteSignature(opts: PaletteTextureOptions): string {\n const s = opts.stops.map((x) => `${x.color}@${x.pos.toFixed(3)}`).join(\",\");\n return `${s}|${opts.edgeColor}|${opts.edgeAmount.toFixed(3)}`;\n}\n\n/** Sampling config shared by every palette-texture source (canvas, image, LUT, video):\n * sRGB (the GPU linearizes on sample), linear filtering, clamped edges, no mipmaps. */\nexport function configurePaletteTexture<T extends THREE.Texture>(tex: T): T {\n tex.colorSpace = THREE.SRGBColorSpace;\n tex.minFilter = THREE.LinearFilter;\n tex.magFilter = THREE.LinearFilter;\n tex.wrapS = THREE.ClampToEdgeWrapping;\n tex.wrapT = THREE.ClampToEdgeWrapping;\n tex.generateMipmaps = false;\n return tex;\n}\n\n/** Wrap a canvas as a sampling-ready CanvasTexture. */\nexport function canvasToTexture(canvas: HTMLCanvasElement): THREE.CanvasTexture {\n return configurePaletteTexture(new THREE.CanvasTexture(canvas));\n}\n\n/** Build a ready-to-use CanvasTexture from gradient stops + edge tint. */\nexport function buildPaletteTexture(opts: PaletteTextureOptions): THREE.CanvasTexture {\n return canvasToTexture(buildPaletteCanvas(opts));\n}\n\nexport interface BackgroundGradientOptions {\n stops: ColorStop[];\n type: BasicGradientType;\n angle: number;\n width: number;\n height: number;\n}\n\n/** Draw an export-ready linear, radial, or conic background gradient. */\nexport function buildBackgroundGradientCanvas(opts: BackgroundGradientOptions): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(opts.width));\n canvas.height = Math.max(1, Math.round(opts.height));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return canvas;\n\n const { width, height } = canvas;\n const cx = width / 2;\n const cy = height / 2;\n const angle = (opts.angle * Math.PI) / 180;\n let gradient: CanvasGradient;\n if (opts.type === \"radial\") {\n gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.hypot(cx, cy));\n } else if (opts.type === \"conic\") {\n gradient = ctx.createConicGradient(angle, cx, cy);\n } else {\n const radius = Math.abs(Math.sin(angle)) * cx + Math.abs(Math.cos(angle)) * cy;\n const dx = Math.sin(angle) * radius;\n const dy = -Math.cos(angle) * radius;\n gradient = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);\n }\n\n const stops = [...opts.stops].sort((a, b) => a.pos - b.pos);\n if (stops.length === 0) gradient.addColorStop(0, \"#ffffff\");\n else for (const stop of stops) gradient.addColorStop(clamp01(stop.pos), stop.color);\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, width, height);\n return canvas;\n}\n\n/** Linear (0–1) → sRGB byte (0–255). */\nfunction linearToSrgbByte(linear: number): number {\n const c = clamp01(linear);\n const srgb = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;\n return Math.round(clamp01(srgb) * 255);\n}\n\n/**\n * The single source of truth for mesh-gradient rendering — shared by the background\n * (buildBackgroundMeshCanvas) and the on-canvas MeshGradientEditor preview, and mirroring the\n * wave's meshGradient shader. An inverse-distance-weighted blend of coloured points, computed in\n * LINEAR RGB with y measured UP (point.y = 1 is the top). Returns an ImageData to putImageData\n * onto any 2D context.\n */\nexport function renderMeshGradient(\n points: MeshGradientPoint[],\n softness: number,\n width: number,\n height: number,\n): ImageData {\n const w = Math.max(1, Math.round(width));\n const h = Math.max(1, Math.round(height));\n const image = new ImageData(w, h);\n const data = image.data;\n const pts = points.length >= 2 ? points : createDefaultMeshPoints();\n const colors = pts.map((p) => new THREE.Color(p.color)); // .r/.g/.b are LINEAR\n const exponent = 4.8 + (1.35 - 4.8) * clamp01(softness);\n for (let py = 0; py < h; py++) {\n const y = 1 - py / Math.max(1, h - 1);\n for (let px = 0; px < w; px++) {\n const x = px / Math.max(1, w - 1);\n let r = 0;\n let g = 0;\n let b = 0;\n let weightSum = 0;\n for (let i = 0; i < pts.length; i++) {\n const influence = Math.max(pts[i].influence, 0.05);\n const distance = Math.hypot(x - pts[i].x, y - pts[i].y) / influence;\n const weight = 1 / (Math.pow(Math.max(distance, 0.012), exponent) + 0.002);\n r += colors[i].r * weight;\n g += colors[i].g * weight;\n b += colors[i].b * weight;\n weightSum += weight;\n }\n const iw = Math.max(weightSum, 0.0001);\n const off = (py * w + px) * 4;\n data[off] = linearToSrgbByte(r / iw);\n data[off + 1] = linearToSrgbByte(g / iw);\n data[off + 2] = linearToSrgbByte(b / iw);\n data[off + 3] = 255;\n }\n }\n return image;\n}\n\n/**\n * Draw a mesh-gradient background. Mesh gradients are low-frequency, so we render at a small\n * internal size (capped ~320 px) via renderMeshGradient and scale up smoothly.\n */\nexport function buildBackgroundMeshCanvas(\n points: MeshGradientPoint[],\n softness: number,\n width: number,\n height: number,\n): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(width));\n canvas.height = Math.max(1, Math.round(height));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return canvas;\n\n const scale = Math.min(1, 320 / Math.max(canvas.width, canvas.height));\n const lw = Math.max(2, Math.round(canvas.width * scale));\n const lh = Math.max(2, Math.round(canvas.height * scale));\n const buf = document.createElement(\"canvas\");\n buf.width = lw;\n buf.height = lh;\n const bctx = buf.getContext(\"2d\");\n if (!bctx) return canvas;\n bctx.putImageData(renderMeshGradient(points, softness, lw, lh), 0, 0);\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(buf, 0, 0, lw, lh, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\n/** Fit a built-in canvas or uploaded image into a background-sized canvas. */\nexport function buildBackgroundImageCanvas(\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n width: number,\n height: number,\n fit: BackgroundImageFit,\n matte: string,\n zoom = 1,\n positionX = 0,\n positionY = 0,\n): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(width));\n canvas.height = Math.max(1, Math.round(height));\n drawBackgroundMediaFrame(\n canvas,\n source,\n sourceWidth,\n sourceHeight,\n fit,\n matte,\n zoom,\n positionX,\n positionY,\n );\n return canvas;\n}\n\n/** Draw one image or video frame into an existing fitted background canvas. */\nexport function drawBackgroundMediaFrame(\n canvas: HTMLCanvasElement,\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n fit: BackgroundImageFit,\n matte: string,\n zoom = 1,\n positionX = 0,\n positionY = 0,\n): void {\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.fillStyle = matte;\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n const safeZoom = Math.max(0.1, zoom);\n const baseWidth =\n fit === \"stretch\"\n ? canvas.width\n : sourceWidth *\n (fit === \"contain\"\n ? Math.min(canvas.width / sourceWidth, canvas.height / sourceHeight)\n : Math.max(canvas.width / sourceWidth, canvas.height / sourceHeight));\n const baseHeight =\n fit === \"stretch\"\n ? canvas.height\n : sourceHeight *\n (fit === \"contain\"\n ? Math.min(canvas.width / sourceWidth, canvas.height / sourceHeight)\n : Math.max(canvas.width / sourceWidth, canvas.height / sourceHeight));\n const drawWidth = baseWidth * safeZoom;\n const drawHeight = baseHeight * safeZoom;\n const offsetX = (positionX / 100) * canvas.width;\n const offsetY = (positionY / 100) * canvas.height;\n ctx.drawImage(\n source,\n (canvas.width - drawWidth) / 2 + offsetX,\n (canvas.height - drawHeight) / 2 + offsetY,\n drawWidth,\n drawHeight,\n );\n}\n\n// ---- Built-in palette maps (pick via config.paletteSource) ----\n\nexport interface PaletteMapDef {\n label: string;\n /** \"gradient\" = a color-stop preset (1-D ramp + edge tint, reproducible with stops);\n * \"image\" = a true 2-D texture (build()) that the stops can't reproduce. */\n kind: \"gradient\" | \"image\";\n stops?: ColorStop[];\n edgeColor?: string;\n edgeAmount?: number;\n build?: (resolution?: number) => HTMLCanvasElement;\n}\n\nconst mk = (pairs: Array<[string, number]>): ColorStop[] =>\n pairs.map(([color, pos]) => ({ color, pos }));\n\n// ---- A genuine 2-D image map (procedural nebula): organic colour patches that vary in\n// BOTH axes via domain-warped value noise — the kind of thing flat stops can't make. ----\n\nfunction valueNoise2D(seed: number): (x: number, y: number) => number {\n const hash = (x: number, y: number): number => {\n let h = (x * 374761393 + y * 668265263 + seed * 1442695041) | 0;\n h = Math.imul(h ^ (h >>> 13), 1274126177);\n return ((h ^ (h >>> 16)) >>> 0) / 4294967295;\n };\n return (x, y) => {\n const xi = Math.floor(x);\n const yi = Math.floor(y);\n const u = smoothstep(x - xi);\n const v = smoothstep(y - yi);\n const a = hash(xi, yi);\n const b = hash(xi + 1, yi);\n const c = hash(xi, yi + 1);\n const d = hash(xi + 1, yi + 1);\n return a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v;\n };\n}\n\nfunction fbm(n: (x: number, y: number) => number, x: number, y: number, oct: number): number {\n let s = 0;\n let amp = 0.5;\n let f = 1;\n let tot = 0;\n for (let i = 0; i < oct; i++) {\n s += amp * n(x * f, y * f);\n tot += amp;\n f *= 2;\n amp *= 0.5;\n }\n return s / tot;\n}\n\nconst nebulaCache = new Map<number, HTMLCanvasElement>();\nfunction buildNebulaCanvas(resolution = 220): HTMLCanvasElement {\n const N = Math.max(64, Math.min(1280, Math.round(resolution)));\n const cached = nebulaCache.get(N);\n if (cached) return cached;\n const cv = document.createElement(\"canvas\");\n cv.width = N;\n cv.height = N;\n const ctx = cv.getContext(\"2d\");\n if (!ctx) return cv;\n const img = ctx.createImageData(N, N);\n const field = valueNoise2D(11);\n const warp = valueNoise2D(91);\n const bloomN = valueNoise2D(47);\n const COLORS: Array<[number, [number, number, number]]> = [\n [0.0, [34, 26, 92]], // deep indigo\n [0.24, [104, 48, 196]], // violet\n [0.46, [226, 70, 158]], // magenta\n [0.64, [255, 122, 92]], // coral\n [0.82, [255, 202, 110]], // gold\n [1.0, [70, 196, 188]], // teal\n ];\n const ramp = (t: number): [number, number, number] => {\n t = clamp01(t);\n for (let i = 1; i < COLORS.length; i++) {\n if (t <= COLORS[i][0]) {\n const [p0, c0] = COLORS[i - 1];\n const [p1, c1] = COLORS[i];\n const k = (t - p0) / (p1 - p0);\n return [\n c0[0] + (c1[0] - c0[0]) * k,\n c0[1] + (c1[1] - c0[1]) * k,\n c0[2] + (c1[2] - c0[2]) * k,\n ];\n }\n }\n return COLORS[COLORS.length - 1][1];\n };\n const S = 3.0;\n for (let y = 0; y < N; y++) {\n for (let x = 0; x < N; x++) {\n const u = (x / N) * S;\n const v = (y / N) * S;\n const wx = u + 0.7 * warp(u * 0.7 + 5, v * 0.7);\n const wy = v + 0.7 * warp(u * 0.7, v * 0.7 + 9);\n const f = fbm(field, wx, wy, 4);\n let [r, g, b] = ramp(f);\n const bloom = Math.max(0, fbm(bloomN, wx * 1.6 + 3, wy * 1.6, 3) - 0.55) * 1.7;\n r = r * (1 - bloom) + 70 * bloom;\n g = g * (1 - bloom) + 210 * bloom;\n b = b * (1 - bloom) + 200 * bloom;\n const i = (y * N + x) * 4;\n img.data[i] = r;\n img.data[i + 1] = g;\n img.data[i + 2] = b;\n img.data[i + 3] = 255;\n }\n }\n ctx.putImageData(img, 0, 0);\n nebulaCache.set(N, cv);\n return cv;\n}\n\nconst imageMapCache = new Map<string, HTMLCanvasElement>();\n\nfunction cachedImageMap(\n id: string,\n width: number,\n height: number,\n paint: (ctx: CanvasRenderingContext2D, width: number, height: number) => void,\n): HTMLCanvasElement {\n const cacheKey = `${id}|${width}x${height}`;\n const cached = imageMapCache.get(cacheKey);\n if (cached) return cached;\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) paint(ctx, width, height);\n imageMapCache.set(cacheKey, canvas);\n return canvas;\n}\n\nfunction buildVaporwaveCanvas(resolution = 240): HTMLCanvasElement {\n const width = Math.max(120, Math.min(2048, Math.round(resolution)));\n const height = Math.round((width * 2) / 3);\n return cachedImageMap(\"vaporwave\", width, height, (ctx, canvasWidth, canvasHeight) => {\n const unit = canvasWidth / 240;\n const sky = ctx.createLinearGradient(0, 0, 0, canvasHeight);\n sky.addColorStop(0, \"#120638\");\n sky.addColorStop(0.56, \"#8e2de2\");\n sky.addColorStop(1, \"#ff2fa8\");\n ctx.fillStyle = sky;\n ctx.fillRect(0, 0, canvasWidth, canvasHeight);\n\n const sunY = canvasHeight * 0.48;\n const sunRadius = canvasHeight * 0.28;\n const sun = ctx.createLinearGradient(0, sunY - sunRadius, 0, sunY + sunRadius);\n sun.addColorStop(0, \"#fff66d\");\n sun.addColorStop(1, \"#ff5cbe\");\n ctx.fillStyle = sun;\n ctx.beginPath();\n ctx.arc(canvasWidth / 2, sunY, sunRadius, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.fillStyle = \"#3b126c\";\n for (let y = sunY + 3 * unit; y < sunY + sunRadius; y += 9 * unit) {\n ctx.fillRect(canvasWidth / 2 - sunRadius, y, sunRadius * 2, 4 * unit);\n }\n\n const horizon = canvasHeight * 0.72;\n ctx.fillStyle = \"#09051f\";\n ctx.fillRect(0, horizon, canvasWidth, canvasHeight - horizon);\n ctx.strokeStyle = \"#19e3ff\";\n ctx.lineWidth = Math.max(1, unit);\n ctx.globalAlpha = 0.72;\n for (let x = -canvasWidth; x <= canvasWidth * 2; x += canvasWidth / 12) {\n ctx.beginPath();\n ctx.moveTo(canvasWidth / 2, horizon);\n ctx.lineTo(x, canvasHeight);\n ctx.stroke();\n }\n for (let row = 0; row < 7; row++) {\n const t = row / 6;\n const y = horizon + (canvasHeight - horizon) * t * t;\n ctx.beginPath();\n ctx.moveTo(0, y);\n ctx.lineTo(canvasWidth, y);\n ctx.stroke();\n }\n ctx.globalAlpha = 1;\n });\n}\n\nfunction buildKaleidoscopeCanvas(resolution = 220): HTMLCanvasElement {\n const size = Math.max(110, Math.min(2048, Math.round(resolution)));\n return cachedImageMap(\"kaleidoscope\", size, size, (ctx, width, height) => {\n const unit = width / 220;\n const colors = [\"#00e5ff\", \"#6c3bff\", \"#ff2ca8\", \"#ff7a00\", \"#ffe600\", \"#16f7a6\"];\n const cx = width / 2;\n const cy = height / 2;\n const radius = Math.hypot(width, height);\n const slices = 24;\n for (let i = 0; i < slices; i++) {\n const a0 = (i / slices) * Math.PI * 2;\n const a1 = ((i + 1) / slices) * Math.PI * 2;\n ctx.fillStyle = colors[(i * 5) % colors.length];\n ctx.beginPath();\n ctx.moveTo(cx, cy);\n ctx.lineTo(cx + Math.cos(a0) * radius, cy + Math.sin(a0) * radius);\n ctx.lineTo(cx + Math.cos(a1) * radius, cy + Math.sin(a1) * radius);\n ctx.closePath();\n ctx.fill();\n }\n for (let ring = 1; ring <= 4; ring++) {\n ctx.strokeStyle = ring % 2 ? \"rgba(255,255,255,.55)\" : \"rgba(8,5,35,.6)\";\n ctx.lineWidth = 5 * unit;\n ctx.beginPath();\n ctx.arc(cx, cy, ring * 25 * unit, 0, Math.PI * 2);\n ctx.stroke();\n }\n });\n}\n\n/** Named palette maps. \"gradient\" kinds are stop presets; \"image\" kinds are true 2-D maps. */\nexport const PALETTE_MAPS: Record<string, PaletteMapDef> = {\n palestine: {\n label: \"Palestine\",\n kind: \"gradient\",\n stops: mk([\n [\"#000000\", 0],\n [\"#f7f7f2\", 0.34],\n [\"#149954\", 0.67],\n [\"#e4312b\", 1],\n ]),\n edgeColor: \"#e4312b\",\n edgeAmount: 0.22,\n },\n grandLine: {\n label: \"Grand Line\",\n kind: \"gradient\",\n stops: mk([\n [\"#071b33\", 0],\n [\"#087ea4\", 0.22],\n [\"#45d4c5\", 0.4],\n [\"#f4d35e\", 0.58],\n [\"#f2b84b\", 0.72],\n [\"#d62828\", 0.86],\n [\"#18130f\", 1],\n ]),\n edgeColor: \"#061426\",\n edgeAmount: 0.28,\n },\n vaporwave: { label: \"Vaporwave Sunset\", kind: \"image\", build: buildVaporwaveCanvas },\n kaleidoscope: { label: \"Kaleidoscope\", kind: \"image\", build: buildKaleidoscopeCanvas },\n nebula: { label: \"Nebula (2D)\", kind: \"image\", build: buildNebulaCanvas },\n sunset: {\n label: \"Sunset\",\n kind: \"gradient\",\n stops: mk([\n [\"#3b1c6b\", 0],\n [\"#8b2fa0\", 0.3],\n [\"#e0457a\", 0.56],\n [\"#ff7a3d\", 0.8],\n [\"#ffd166\", 1],\n ]),\n edgeColor: \"#2a1a6b\",\n edgeAmount: 0.3,\n },\n aurora: {\n label: \"Aurora\",\n kind: \"gradient\",\n stops: mk([\n [\"#0b3d4f\", 0],\n [\"#1fb89e\", 0.3],\n [\"#5ee0a0\", 0.52],\n [\"#4d8ef0\", 0.76],\n [\"#9b5de5\", 1],\n ]),\n edgeColor: \"#0a2540\",\n edgeAmount: 0.35,\n },\n ocean: {\n label: \"Ocean\",\n kind: \"gradient\",\n stops: mk([\n [\"#0a1f4d\", 0],\n [\"#1f6fb8\", 0.36],\n [\"#2bd0d0\", 0.66],\n [\"#a8f0e2\", 1],\n ]),\n edgeColor: \"#061233\",\n edgeAmount: 0.4,\n },\n ember: {\n label: \"Ember\",\n kind: \"gradient\",\n stops: mk([\n [\"#2a0707\", 0],\n [\"#a81e1e\", 0.34],\n [\"#ff5a2e\", 0.64],\n [\"#ffd24a\", 1],\n ]),\n edgeColor: \"#150404\",\n edgeAmount: 0.28,\n },\n iris: {\n label: \"Iris\",\n kind: \"gradient\",\n stops: mk([\n [\"#2e1065\", 0],\n [\"#7c3aed\", 0.34],\n [\"#db2777\", 0.64],\n [\"#f5b8f0\", 1],\n ]),\n edgeColor: \"#1a0840\",\n edgeAmount: 0.35,\n },\n mono: {\n label: \"Mono\",\n kind: \"gradient\",\n stops: mk([\n [\"#16161e\", 0],\n [\"#6b7280\", 0.4],\n [\"#c2c8d2\", 0.72],\n [\"#f6f8fb\", 1],\n ]),\n edgeColor: \"#0a0a12\",\n edgeAmount: 0.2,\n },\n};\n\n/** The canvas for a named map (its 2-D image, or its stops+edge gradient). */\nexport function paletteMapCanvas(def: PaletteMapDef, resolution?: number): HTMLCanvasElement {\n if (def.build) return def.build(resolution);\n return buildPaletteCanvas({\n stops: def.stops ?? [],\n edgeColor: def.edgeColor ?? \"#8e9dff\",\n edgeAmount: def.edgeAmount ?? 0,\n });\n}\n\n/** Load an arbitrary image (URL or object-URL) as a palette texture — \"bring your own map\". */\nexport function loadPaletteImage(url: string): THREE.Texture {\n return configurePaletteTexture(new THREE.TextureLoader().load(url));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"palette.js","names":[],"sources":["../../src/renderer/palette.ts"],"sourcesContent":["import * as THREE from \"three\";\nimport { clamp01 } from \"../util/math\";\nimport { createDefaultMeshPoints } from \"../config/model\";\nimport type {\n BackgroundImageFit,\n BasicGradientType,\n ColorStop,\n MeshGradientPoint,\n} from \"../config/model\";\n\n/**\n * Bakes the gradient stops into a 2D palette texture, sampled in the shader as\n * `texture2D(u_paletteTexture, vec2(uv.x, uv.y))`. The texture is a real image, so\n * colour can vary independently along BOTH axes:\n * - X (uv.x, along the length): the gradient stops.\n * - Y (uv.y, across the width): an \"edge tint\" blended toward both long edges\n */\n\nconst TEX_W = 256; // resolution along the gradient (length)\nconst TEX_H = 64; // resolution across the width\n\nfunction smoothstep(value: number): number {\n return value * value * (3 - 2 * value);\n}\n\n/** \"#rrggbb\" → \"rgba(r,g,b,a)\" for canvas fills with alpha. */\nfunction hexToRgba(hex: string, alpha: number): string {\n const c = new THREE.Color(hex); // parses many formats; .r/.g/.b are linear…\n // …but we want the original sRGB bytes for the canvas, so re-encode.\n const srgb = c.clone().convertLinearToSRGB();\n const r = Math.round(srgb.r * 255);\n const g = Math.round(srgb.g * 255);\n const b = Math.round(srgb.b * 255);\n return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nexport interface PaletteTextureOptions {\n stops: ColorStop[];\n /** Cross-width edge tint colour (e.g. periwinkle). */\n edgeColor: string;\n /** 0 = flat 1-D gradient (no 2nd axis); higher = stronger cool edges. */\n edgeAmount: number;\n}\n\n/** Draw the palette into a 2D canvas. */\nexport function buildPaletteCanvas(opts: PaletteTextureOptions): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = TEX_W;\n canvas.height = TEX_H;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return canvas;\n\n // X axis: the main gradient (stops sorted by position).\n const stops = [...opts.stops].sort((a, b) => a.pos - b.pos);\n const grad = ctx.createLinearGradient(0, 0, TEX_W, 0);\n if (stops.length === 0) {\n grad.addColorStop(0, \"#ffffff\");\n } else {\n for (const s of stops) grad.addColorStop(clamp01(s.pos), s.color);\n }\n ctx.fillStyle = grad;\n ctx.fillRect(0, 0, TEX_W, TEX_H);\n\n // Y axis: blend the edge colour toward both long edges (V-shaped alpha), 0 in the\n // middle — a genuine second dimension of colour.\n const a = clamp01(opts.edgeAmount);\n if (a > 0.001) {\n const eg = ctx.createLinearGradient(0, 0, 0, TEX_H);\n eg.addColorStop(0, hexToRgba(opts.edgeColor, a));\n eg.addColorStop(0.5, hexToRgba(opts.edgeColor, 0));\n eg.addColorStop(1, hexToRgba(opts.edgeColor, a));\n ctx.fillStyle = eg;\n ctx.fillRect(0, 0, TEX_W, TEX_H);\n }\n\n return canvas;\n}\n\n/** A small string that changes whenever the texture would need rebuilding. */\nexport function paletteSignature(opts: PaletteTextureOptions): string {\n const s = opts.stops.map((x) => `${x.color}@${x.pos.toFixed(3)}`).join(\",\");\n return `${s}|${opts.edgeColor}|${opts.edgeAmount.toFixed(3)}`;\n}\n\n/** Sampling config shared by every palette-texture source (canvas, image, LUT, video):\n * sRGB (the GPU linearizes on sample), linear filtering, clamped edges, no mipmaps. */\nexport function configurePaletteTexture<T extends THREE.Texture>(tex: T): T {\n tex.colorSpace = THREE.SRGBColorSpace;\n tex.minFilter = THREE.LinearFilter;\n tex.magFilter = THREE.LinearFilter;\n tex.wrapS = THREE.ClampToEdgeWrapping;\n tex.wrapT = THREE.ClampToEdgeWrapping;\n tex.generateMipmaps = false;\n return tex;\n}\n\n/** Wrap a canvas as a sampling-ready CanvasTexture. */\nexport function canvasToTexture(canvas: HTMLCanvasElement): THREE.CanvasTexture {\n return configurePaletteTexture(new THREE.CanvasTexture(canvas));\n}\n\n/** Build a ready-to-use CanvasTexture from gradient stops + edge tint. */\nexport function buildPaletteTexture(opts: PaletteTextureOptions): THREE.CanvasTexture {\n return canvasToTexture(buildPaletteCanvas(opts));\n}\n\nexport interface BackgroundGradientOptions {\n stops: ColorStop[];\n type: BasicGradientType;\n angle: number;\n width: number;\n height: number;\n}\n\n/** Draw an export-ready linear, radial, or conic background gradient. */\nexport function buildBackgroundGradientCanvas(opts: BackgroundGradientOptions): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(opts.width));\n canvas.height = Math.max(1, Math.round(opts.height));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return canvas;\n\n const { width, height } = canvas;\n const cx = width / 2;\n const cy = height / 2;\n const angle = (opts.angle * Math.PI) / 180;\n let gradient: CanvasGradient;\n if (opts.type === \"radial\") {\n gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.hypot(cx, cy));\n } else if (opts.type === \"conic\") {\n gradient = ctx.createConicGradient(angle, cx, cy);\n } else {\n const radius = Math.abs(Math.sin(angle)) * cx + Math.abs(Math.cos(angle)) * cy;\n const dx = Math.sin(angle) * radius;\n const dy = -Math.cos(angle) * radius;\n gradient = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);\n }\n\n const stops = [...opts.stops].sort((a, b) => a.pos - b.pos);\n if (stops.length === 0) gradient.addColorStop(0, \"#ffffff\");\n else for (const stop of stops) gradient.addColorStop(clamp01(stop.pos), stop.color);\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, width, height);\n return canvas;\n}\n\n/** Linear (0–1) → sRGB byte (0–255). */\nfunction linearToSrgbByte(linear: number): number {\n const c = clamp01(linear);\n const srgb = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;\n return Math.round(clamp01(srgb) * 255);\n}\n\n/**\n * The single source of truth for mesh-gradient rendering — shared by the background\n * (buildBackgroundMeshCanvas) and the on-canvas MeshGradientEditor preview, and mirroring the\n * wave's meshGradient shader. An inverse-distance-weighted blend of coloured points, computed in\n * LINEAR RGB with y measured UP (point.y = 1 is the top). Returns an ImageData to putImageData\n * onto any 2D context.\n */\nexport function renderMeshGradient(\n points: MeshGradientPoint[],\n softness: number,\n width: number,\n height: number,\n): ImageData {\n const w = Math.max(1, Math.round(width));\n const h = Math.max(1, Math.round(height));\n const image = new ImageData(w, h);\n const data = image.data;\n const pts = points.length >= 2 ? points : createDefaultMeshPoints();\n const colors = pts.map((p) => new THREE.Color(p.color)); // .r/.g/.b are LINEAR\n const exponent = 4.8 + (1.35 - 4.8) * clamp01(softness);\n for (let py = 0; py < h; py++) {\n const y = 1 - py / Math.max(1, h - 1);\n for (let px = 0; px < w; px++) {\n const x = px / Math.max(1, w - 1);\n let r = 0;\n let g = 0;\n let b = 0;\n let weightSum = 0;\n for (let i = 0; i < pts.length; i++) {\n const influence = Math.max(pts[i].influence, 0.05);\n const distance = Math.hypot(x - pts[i].x, y - pts[i].y) / influence;\n const weight = 1 / (Math.pow(Math.max(distance, 0.012), exponent) + 0.002);\n r += colors[i].r * weight;\n g += colors[i].g * weight;\n b += colors[i].b * weight;\n weightSum += weight;\n }\n const iw = Math.max(weightSum, 0.0001);\n const off = (py * w + px) * 4;\n data[off] = linearToSrgbByte(r / iw);\n data[off + 1] = linearToSrgbByte(g / iw);\n data[off + 2] = linearToSrgbByte(b / iw);\n data[off + 3] = 255;\n }\n }\n return image;\n}\n\n/**\n * Draw a mesh-gradient background. Mesh gradients are low-frequency, so we render at a small\n * internal size (capped ~320 px) via renderMeshGradient and scale up smoothly.\n */\nexport function buildBackgroundMeshCanvas(\n points: MeshGradientPoint[],\n softness: number,\n width: number,\n height: number,\n): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(width));\n canvas.height = Math.max(1, Math.round(height));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return canvas;\n\n const scale = Math.min(1, 320 / Math.max(canvas.width, canvas.height));\n const lw = Math.max(2, Math.round(canvas.width * scale));\n const lh = Math.max(2, Math.round(canvas.height * scale));\n const buf = document.createElement(\"canvas\");\n buf.width = lw;\n buf.height = lh;\n const bctx = buf.getContext(\"2d\");\n if (!bctx) return canvas;\n bctx.putImageData(renderMeshGradient(points, softness, lw, lh), 0, 0);\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = \"high\";\n ctx.drawImage(buf, 0, 0, lw, lh, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\n/** Fit a built-in canvas or uploaded image into a background-sized canvas. */\nexport function buildBackgroundImageCanvas(\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n width: number,\n height: number,\n fit: BackgroundImageFit,\n matte: string,\n zoom = 1,\n positionX = 0,\n positionY = 0,\n): HTMLCanvasElement {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(width));\n canvas.height = Math.max(1, Math.round(height));\n drawBackgroundMediaFrame(\n canvas,\n source,\n sourceWidth,\n sourceHeight,\n fit,\n matte,\n zoom,\n positionX,\n positionY,\n );\n return canvas;\n}\n\n/** Draw one image or video frame into an existing fitted background canvas. */\nexport function drawBackgroundMediaFrame(\n canvas: HTMLCanvasElement,\n source: CanvasImageSource,\n sourceWidth: number,\n sourceHeight: number,\n fit: BackgroundImageFit,\n matte: string,\n zoom = 1,\n positionX = 0,\n positionY = 0,\n): void {\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.fillStyle = matte;\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n const safeZoom = Math.max(0.1, zoom);\n const baseWidth =\n fit === \"stretch\"\n ? canvas.width\n : sourceWidth *\n (fit === \"contain\"\n ? Math.min(canvas.width / sourceWidth, canvas.height / sourceHeight)\n : Math.max(canvas.width / sourceWidth, canvas.height / sourceHeight));\n const baseHeight =\n fit === \"stretch\"\n ? canvas.height\n : sourceHeight *\n (fit === \"contain\"\n ? Math.min(canvas.width / sourceWidth, canvas.height / sourceHeight)\n : Math.max(canvas.width / sourceWidth, canvas.height / sourceHeight));\n const drawWidth = baseWidth * safeZoom;\n const drawHeight = baseHeight * safeZoom;\n const offsetX = (positionX / 100) * canvas.width;\n const offsetY = (positionY / 100) * canvas.height;\n ctx.drawImage(\n source,\n (canvas.width - drawWidth) / 2 + offsetX,\n (canvas.height - drawHeight) / 2 + offsetY,\n drawWidth,\n drawHeight,\n );\n}\n\n// ---- Built-in palette maps (pick via config.paletteSource) ----\n\nexport interface PaletteMapDef {\n label: string;\n /** \"gradient\" = a color-stop preset (1-D ramp + edge tint, reproducible with stops);\n * \"image\" = a true 2-D texture (build()) that the stops can't reproduce. */\n kind: \"gradient\" | \"image\";\n stops?: ColorStop[];\n edgeColor?: string;\n edgeAmount?: number;\n build?: (resolution?: number) => HTMLCanvasElement;\n}\n\nconst mk = (pairs: Array<[string, number]>): ColorStop[] =>\n pairs.map(([color, pos]) => ({ color, pos }));\n\n// ---- A genuine 2-D image map (procedural nebula): organic colour patches that vary in\n// BOTH axes via domain-warped value noise — the kind of thing flat stops can't make. ----\n\nfunction valueNoise2D(seed: number): (x: number, y: number) => number {\n const hash = (x: number, y: number): number => {\n let h = (x * 374761393 + y * 668265263 + seed * 1442695041) | 0;\n h = Math.imul(h ^ (h >>> 13), 1274126177);\n return ((h ^ (h >>> 16)) >>> 0) / 4294967295;\n };\n return (x, y) => {\n const xi = Math.floor(x);\n const yi = Math.floor(y);\n const u = smoothstep(x - xi);\n const v = smoothstep(y - yi);\n const a = hash(xi, yi);\n const b = hash(xi + 1, yi);\n const c = hash(xi, yi + 1);\n const d = hash(xi + 1, yi + 1);\n return a * (1 - u) * (1 - v) + b * u * (1 - v) + c * (1 - u) * v + d * u * v;\n };\n}\n\nfunction fbm(n: (x: number, y: number) => number, x: number, y: number, oct: number): number {\n let s = 0;\n let amp = 0.5;\n let f = 1;\n let tot = 0;\n for (let i = 0; i < oct; i++) {\n s += amp * n(x * f, y * f);\n tot += amp;\n f *= 2;\n amp *= 0.5;\n }\n return s / tot;\n}\n\nconst nebulaCache = new Map<number, HTMLCanvasElement>();\nfunction buildNebulaCanvas(resolution = 220): HTMLCanvasElement {\n const N = Math.max(64, Math.min(1280, Math.round(resolution)));\n const cached = nebulaCache.get(N);\n if (cached) return cached;\n const cv = document.createElement(\"canvas\");\n cv.width = N;\n cv.height = N;\n const ctx = cv.getContext(\"2d\");\n if (!ctx) return cv;\n const img = ctx.createImageData(N, N);\n const field = valueNoise2D(11);\n const warp = valueNoise2D(91);\n const bloomN = valueNoise2D(47);\n const COLORS: Array<[number, [number, number, number]]> = [\n [0.0, [34, 26, 92]], // deep indigo\n [0.24, [104, 48, 196]], // violet\n [0.46, [226, 70, 158]], // magenta\n [0.64, [255, 122, 92]], // coral\n [0.82, [255, 202, 110]], // gold\n [1.0, [70, 196, 188]], // teal\n ];\n const ramp = (t: number): [number, number, number] => {\n t = clamp01(t);\n for (let i = 1; i < COLORS.length; i++) {\n if (t <= COLORS[i][0]) {\n const [p0, c0] = COLORS[i - 1];\n const [p1, c1] = COLORS[i];\n const k = (t - p0) / (p1 - p0);\n return [\n c0[0] + (c1[0] - c0[0]) * k,\n c0[1] + (c1[1] - c0[1]) * k,\n c0[2] + (c1[2] - c0[2]) * k,\n ];\n }\n }\n return COLORS[COLORS.length - 1][1];\n };\n const S = 3.0;\n for (let y = 0; y < N; y++) {\n for (let x = 0; x < N; x++) {\n const u = (x / N) * S;\n const v = (y / N) * S;\n const wx = u + 0.7 * warp(u * 0.7 + 5, v * 0.7);\n const wy = v + 0.7 * warp(u * 0.7, v * 0.7 + 9);\n const f = fbm(field, wx, wy, 4);\n let [r, g, b] = ramp(f);\n const bloom = Math.max(0, fbm(bloomN, wx * 1.6 + 3, wy * 1.6, 3) - 0.55) * 1.7;\n r = r * (1 - bloom) + 70 * bloom;\n g = g * (1 - bloom) + 210 * bloom;\n b = b * (1 - bloom) + 200 * bloom;\n const i = (y * N + x) * 4;\n img.data[i] = r;\n img.data[i + 1] = g;\n img.data[i + 2] = b;\n img.data[i + 3] = 255;\n }\n }\n ctx.putImageData(img, 0, 0);\n nebulaCache.set(N, cv);\n return cv;\n}\n\nconst imageMapCache = new Map<string, HTMLCanvasElement>();\n\nfunction cachedImageMap(\n id: string,\n width: number,\n height: number,\n paint: (ctx: CanvasRenderingContext2D, width: number, height: number) => void,\n): HTMLCanvasElement {\n const cacheKey = `${id}|${width}x${height}`;\n const cached = imageMapCache.get(cacheKey);\n if (cached) return cached;\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) paint(ctx, width, height);\n imageMapCache.set(cacheKey, canvas);\n return canvas;\n}\n\nfunction buildVaporwaveCanvas(resolution = 240): HTMLCanvasElement {\n const width = Math.max(120, Math.min(2048, Math.round(resolution)));\n const height = Math.round((width * 2) / 3);\n return cachedImageMap(\"vaporwave\", width, height, (ctx, canvasWidth, canvasHeight) => {\n const unit = canvasWidth / 240;\n const sky = ctx.createLinearGradient(0, 0, 0, canvasHeight);\n sky.addColorStop(0, \"#120638\");\n sky.addColorStop(0.56, \"#8e2de2\");\n sky.addColorStop(1, \"#ff2fa8\");\n ctx.fillStyle = sky;\n ctx.fillRect(0, 0, canvasWidth, canvasHeight);\n\n const sunY = canvasHeight * 0.48;\n const sunRadius = canvasHeight * 0.28;\n const sun = ctx.createLinearGradient(0, sunY - sunRadius, 0, sunY + sunRadius);\n sun.addColorStop(0, \"#fff66d\");\n sun.addColorStop(1, \"#ff5cbe\");\n ctx.fillStyle = sun;\n ctx.beginPath();\n ctx.arc(canvasWidth / 2, sunY, sunRadius, 0, Math.PI * 2);\n ctx.fill();\n\n ctx.fillStyle = \"#3b126c\";\n for (let y = sunY + 3 * unit; y < sunY + sunRadius; y += 9 * unit) {\n ctx.fillRect(canvasWidth / 2 - sunRadius, y, sunRadius * 2, 4 * unit);\n }\n\n const horizon = canvasHeight * 0.72;\n ctx.fillStyle = \"#09051f\";\n ctx.fillRect(0, horizon, canvasWidth, canvasHeight - horizon);\n ctx.strokeStyle = \"#19e3ff\";\n ctx.lineWidth = Math.max(1, unit);\n ctx.globalAlpha = 0.72;\n for (let x = -canvasWidth; x <= canvasWidth * 2; x += canvasWidth / 12) {\n ctx.beginPath();\n ctx.moveTo(canvasWidth / 2, horizon);\n ctx.lineTo(x, canvasHeight);\n ctx.stroke();\n }\n for (let row = 0; row < 7; row++) {\n const t = row / 6;\n const y = horizon + (canvasHeight - horizon) * t * t;\n ctx.beginPath();\n ctx.moveTo(0, y);\n ctx.lineTo(canvasWidth, y);\n ctx.stroke();\n }\n ctx.globalAlpha = 1;\n });\n}\n\nfunction buildKaleidoscopeCanvas(resolution = 220): HTMLCanvasElement {\n const size = Math.max(110, Math.min(2048, Math.round(resolution)));\n return cachedImageMap(\"kaleidoscope\", size, size, (ctx, width, height) => {\n const unit = width / 220;\n const colors = [\"#00e5ff\", \"#6c3bff\", \"#ff2ca8\", \"#ff7a00\", \"#ffe600\", \"#16f7a6\"];\n const cx = width / 2;\n const cy = height / 2;\n const radius = Math.hypot(width, height);\n const slices = 24;\n for (let i = 0; i < slices; i++) {\n const a0 = (i / slices) * Math.PI * 2;\n const a1 = ((i + 1) / slices) * Math.PI * 2;\n ctx.fillStyle = colors[(i * 5) % colors.length];\n ctx.beginPath();\n ctx.moveTo(cx, cy);\n ctx.lineTo(cx + Math.cos(a0) * radius, cy + Math.sin(a0) * radius);\n ctx.lineTo(cx + Math.cos(a1) * radius, cy + Math.sin(a1) * radius);\n ctx.closePath();\n ctx.fill();\n }\n for (let ring = 1; ring <= 4; ring++) {\n ctx.strokeStyle = ring % 2 ? \"rgba(255,255,255,.55)\" : \"rgba(8,5,35,.6)\";\n ctx.lineWidth = 5 * unit;\n ctx.beginPath();\n ctx.arc(cx, cy, ring * 25 * unit, 0, Math.PI * 2);\n ctx.stroke();\n }\n });\n}\n\n/** Named palette maps. \"gradient\" kinds are stop presets; \"image\" kinds are true 2-D maps. */\nexport const PALETTE_MAPS: Record<string, PaletteMapDef> = {\n palestine: {\n label: \"Palestine\",\n kind: \"gradient\",\n stops: mk([\n [\"#000000\", 0],\n [\"#f7f7f2\", 0.34],\n [\"#149954\", 0.67],\n [\"#e4312b\", 1],\n ]),\n edgeColor: \"#e4312b\",\n edgeAmount: 0.22,\n },\n spain: {\n label: \"Spain\",\n kind: \"gradient\",\n stops: mk([\n [\"#aa151b\", 0], // crimson — top stripe\n [\"#aa151b\", 0.24], // hold the red band\n [\"#f1bf00\", 0.34], // into gold\n [\"#f1bf00\", 0.66], // hold the wide gold band (middle 50%)\n [\"#aa151b\", 0.76], // back to red\n [\"#aa151b\", 1], // crimson — bottom stripe\n ]),\n edgeColor: \"#7a0f14\", // deep oxblood edge tint (mirrors Palestine's red edge)\n edgeAmount: 0.22,\n },\n grandLine: {\n label: \"Grand Line\",\n kind: \"gradient\",\n stops: mk([\n [\"#071b33\", 0],\n [\"#087ea4\", 0.22],\n [\"#45d4c5\", 0.4],\n [\"#f4d35e\", 0.58],\n [\"#f2b84b\", 0.72],\n [\"#d62828\", 0.86],\n [\"#18130f\", 1],\n ]),\n edgeColor: \"#061426\",\n edgeAmount: 0.28,\n },\n vaporwave: { label: \"Vaporwave Sunset\", kind: \"image\", build: buildVaporwaveCanvas },\n kaleidoscope: { label: \"Kaleidoscope\", kind: \"image\", build: buildKaleidoscopeCanvas },\n nebula: { label: \"Nebula (2D)\", kind: \"image\", build: buildNebulaCanvas },\n sunset: {\n label: \"Sunset\",\n kind: \"gradient\",\n stops: mk([\n [\"#3b1c6b\", 0],\n [\"#8b2fa0\", 0.3],\n [\"#e0457a\", 0.56],\n [\"#ff7a3d\", 0.8],\n [\"#ffd166\", 1],\n ]),\n edgeColor: \"#2a1a6b\",\n edgeAmount: 0.3,\n },\n aurora: {\n label: \"Aurora\",\n kind: \"gradient\",\n stops: mk([\n [\"#0b3d4f\", 0],\n [\"#1fb89e\", 0.3],\n [\"#5ee0a0\", 0.52],\n [\"#4d8ef0\", 0.76],\n [\"#9b5de5\", 1],\n ]),\n edgeColor: \"#0a2540\",\n edgeAmount: 0.35,\n },\n ocean: {\n label: \"Ocean\",\n kind: \"gradient\",\n stops: mk([\n [\"#0a1f4d\", 0],\n [\"#1f6fb8\", 0.36],\n [\"#2bd0d0\", 0.66],\n [\"#a8f0e2\", 1],\n ]),\n edgeColor: \"#061233\",\n edgeAmount: 0.4,\n },\n ember: {\n label: \"Ember\",\n kind: \"gradient\",\n stops: mk([\n [\"#2a0707\", 0],\n [\"#a81e1e\", 0.34],\n [\"#ff5a2e\", 0.64],\n [\"#ffd24a\", 1],\n ]),\n edgeColor: \"#150404\",\n edgeAmount: 0.28,\n },\n iris: {\n label: \"Iris\",\n kind: \"gradient\",\n stops: mk([\n [\"#2e1065\", 0],\n [\"#7c3aed\", 0.34],\n [\"#db2777\", 0.64],\n [\"#f5b8f0\", 1],\n ]),\n edgeColor: \"#1a0840\",\n edgeAmount: 0.35,\n },\n mono: {\n label: \"Mono\",\n kind: \"gradient\",\n stops: mk([\n [\"#16161e\", 0],\n [\"#6b7280\", 0.4],\n [\"#c2c8d2\", 0.72],\n [\"#f6f8fb\", 1],\n ]),\n edgeColor: \"#0a0a12\",\n edgeAmount: 0.2,\n },\n};\n\n/** The canvas for a named map (its 2-D image, or its stops+edge gradient). */\nexport function paletteMapCanvas(def: PaletteMapDef, resolution?: number): HTMLCanvasElement {\n if (def.build) return def.build(resolution);\n return buildPaletteCanvas({\n stops: def.stops ?? [],\n edgeColor: def.edgeColor ?? \"#8e9dff\",\n edgeAmount: def.edgeAmount ?? 0,\n });\n}\n\n/** Load an arbitrary image (URL or object-URL) as a palette texture — \"bring your own map\". */\nexport function loadPaletteImage(url: string): THREE.Texture {\n return configurePaletteTexture(new THREE.TextureLoader().load(url));\n}\n"],"mappings":"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"}
|
package/dist/renderer/shaders.js
CHANGED
|
@@ -171,6 +171,31 @@ varying vec3 vWorldPos;
|
|
|
171
171
|
varying vec3 vViewDir;
|
|
172
172
|
varying vec4 vClipPosition; // = gl_Position, for the wireframe theme's depth fade
|
|
173
173
|
|
|
174
|
+
// Pointer field (optional, additive). ALL declarations here sit behind POINTER_FX so a wave with
|
|
175
|
+
// no interaction config compiles the exact same program (JS-side uniform entries are always present
|
|
176
|
+
// — see makeUniforms — but three only uploads uniforms the compiled program actually declares).
|
|
177
|
+
#ifdef POINTER_FX
|
|
178
|
+
uniform vec2 uPointer; // smoothed pointer, NDC (-1..1)
|
|
179
|
+
uniform float uPointerActive; // presence ramp 0..1 × per-wave influence
|
|
180
|
+
uniform float uPointerRadius; // falloff radius in NDC-y units (config radius × 2)
|
|
181
|
+
uniform float uPointerAspect; // drawing-buffer dw/dh (circular screen falloff)
|
|
182
|
+
uniform float uPointerAgitate;
|
|
183
|
+
uniform float uPointerPush; // signed membrane dome at the cursor (+ repel / − attract)
|
|
184
|
+
uniform float uPointerWake; // drag-wake trough amplitude (behind the moving cursor)
|
|
185
|
+
uniform vec2 uPointerVel; // smoothed pointer velocity, NDC/s (drag-wake direction)
|
|
186
|
+
varying float vPointerFall; // falloff × presence — consumed by both fragment themes
|
|
187
|
+
#ifdef POINTER_RIPPLES
|
|
188
|
+
uniform vec2 uRippleOrigin[4]; // NDC
|
|
189
|
+
uniform float uRippleAge[4]; // seconds since spawn (CPU-computed)
|
|
190
|
+
uniform float uRippleAmp[4]; // shared 0..1 decay envelope per slot (CPU-computed; 0 = slot free)
|
|
191
|
+
uniform float uPointerRipple; // THIS wave's ripple amplitude (scales the shared envelope)
|
|
192
|
+
const float RIPPLE_WAVE_SPEED = 0.85; // NDC/s the ring crest travels outward
|
|
193
|
+
const float RIPPLE_SIGMA = 0.14; // gaussian half-width of the travelling packet (NDC)
|
|
194
|
+
const float RIPPLE_FREQ = 11.0; // oscillation within the packet (one crest + faint troughs)
|
|
195
|
+
const float RIPPLE_MAX_R = 1.2; // reach where the crest has fully left the frame
|
|
196
|
+
#endif
|
|
197
|
+
#endif
|
|
198
|
+
|
|
174
199
|
// expStep: a falloff from 1 (at x=0) toward 0, sharpness set by n. The
|
|
175
200
|
// max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n
|
|
176
201
|
// just concentrates the twist toward the OTHER end instead.
|
|
@@ -250,6 +275,57 @@ void main(){
|
|
|
250
275
|
pos = (vec4(pos, 1.0) * rotB).xyz;
|
|
251
276
|
pos = (vec4(pos, 1.0) * rotC).xyz;
|
|
252
277
|
|
|
278
|
+
#ifdef POINTER_FX
|
|
279
|
+
// Pointer field: displace along the wave's own (post-twist) up-axis, weighted by a circular
|
|
280
|
+
// screen-space falloff around the smoothed cursor. Everything here is ADDITIVE and fenced, so
|
|
281
|
+
// the shared path above/below is untouched and byte-identical when POINTER_FX is off.
|
|
282
|
+
vec4 preClip = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0);
|
|
283
|
+
vec2 dp = (preClip.xy / max(preClip.w, 1.0e-6) - uPointer) * vec2(uPointerAspect, 1.0);
|
|
284
|
+
float fall = smoothstep(uPointerRadius, 0.0, length(dp));
|
|
285
|
+
vPointerFall = fall * uPointerActive;
|
|
286
|
+
// Displacement axis = local +Y carried through the SAME three twist rotations as pos (row-vector
|
|
287
|
+
// convention). Rotations are linear, so post-twist axis displacement equals pre-twist Y displacement.
|
|
288
|
+
vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;
|
|
289
|
+
// Agitation: a fast churn octave near the cursor (additive — never rewrites base noise t, which
|
|
290
|
+
// would force restructuring the shared path). Loop-safe under both time variants.
|
|
291
|
+
#ifdef LOOP_MOTION
|
|
292
|
+
float disp = uPointerAgitate * vPointerFall
|
|
293
|
+
* simplexNoise(vec2(pos.x * uDispFreqX * 3.0, pos.z * uDispFreqZ * 3.0) + loopOff * 4.0);
|
|
294
|
+
#else
|
|
295
|
+
float disp = uPointerAgitate * vPointerFall
|
|
296
|
+
* simplexNoise(vec2(pos.x * uDispFreqX * 3.0 + t * 4.0, pos.z * uDispFreqZ * 3.0));
|
|
297
|
+
#endif
|
|
298
|
+
// Membrane push/pull: a smooth dome (vPointerFall is the falloff) that swells toward you (+ repel)
|
|
299
|
+
// or dents away (− attract) at the cursor, riding along with the sprung field.
|
|
300
|
+
disp += uPointerPush * vPointerFall;
|
|
301
|
+
// Drag-wake: pull the surface just BEHIND the moving cursor into a trailing trough. dp points
|
|
302
|
+
// from cursor to vertex; "behind" is how far the vertex sits opposite the velocity (0 ahead → 1 a
|
|
303
|
+
// radius behind), gated by speed so it only forms while dragging and heals when the cursor stops.
|
|
304
|
+
vec2 velC = uPointerVel * vec2(uPointerAspect, 1.0);
|
|
305
|
+
float wakeSpeed = length(velC);
|
|
306
|
+
if (uPointerWake != 0.0 && wakeSpeed > 1.0e-4) {
|
|
307
|
+
float behind = clamp(dot(-dp, velC) / (wakeSpeed * uPointerRadius), 0.0, 1.0);
|
|
308
|
+
disp -= uPointerWake * vPointerFall * behind * smoothstep(0.05, 0.6, wakeSpeed);
|
|
309
|
+
}
|
|
310
|
+
#ifdef POINTER_RIPPLES
|
|
311
|
+
for (int i = 0; i < 4; i++) {
|
|
312
|
+
if (uRippleAmp[i] > 0.0) {
|
|
313
|
+
float rd = length((preClip.xy / max(preClip.w, 1.0e-6) - uRippleOrigin[i]) * vec2(uPointerAspect, 1.0));
|
|
314
|
+
// A wave PACKET whose crest travels outward at RIPPLE_WAVE_SPEED: a gaussian window centred on
|
|
315
|
+
// the moving front carrying a short oscillation (a raised ring with faint trailing troughs),
|
|
316
|
+
// so the energy radiates instead of throbbing at the click point. The shared uRippleAmp
|
|
317
|
+
// envelope fades the whole packet over its lifetime; reach fades it as the crest leaves frame.
|
|
318
|
+
float front = uRippleAge[i] * RIPPLE_WAVE_SPEED;
|
|
319
|
+
float band = rd - front;
|
|
320
|
+
float packet = exp(-band * band / (2.0 * RIPPLE_SIGMA * RIPPLE_SIGMA)) * cos(band * RIPPLE_FREQ);
|
|
321
|
+
float reach = 1.0 - smoothstep(RIPPLE_MAX_R * 0.7, RIPPLE_MAX_R, front);
|
|
322
|
+
disp += uPointerRipple * uRippleAmp[i] * packet * reach;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
#endif
|
|
326
|
+
pos += dispAxis * disp;
|
|
327
|
+
#endif
|
|
328
|
+
|
|
253
329
|
// The scale / rotation / position transform lives on the mesh (modelMatrix), so the
|
|
254
330
|
// orientation matches THREE's Euler-XYZ rather than an in-shader rotation order.
|
|
255
331
|
vec4 world = modelMatrix * vec4(pos, 1.0);
|
|
@@ -302,6 +378,12 @@ varying vec4 vClipPosition; // clip-space depth (written by the vertex shader fo
|
|
|
302
378
|
#ifdef EDGE_FEATHER
|
|
303
379
|
uniform float uEdgeFeather; // ribbon long-edge softness (only when it differs from the 0.1 default)
|
|
304
380
|
#endif
|
|
381
|
+
#ifdef POINTER_FX
|
|
382
|
+
uniform float uPointerThin; // 0..1 local translucency near the cursor
|
|
383
|
+
uniform float uPointerHue; // degrees, local hue rotation near the cursor
|
|
384
|
+
uniform float uPointerLighten; // -1..1 local brightness lift near the cursor
|
|
385
|
+
varying float vPointerFall; // falloff × presence, written by the vertex shader
|
|
386
|
+
#endif
|
|
305
387
|
|
|
306
388
|
// Cheap value hash for the optional grain overlay (distinct from the simplex hash).
|
|
307
389
|
float grainHash(vec2 p){ return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
|
|
@@ -366,6 +448,12 @@ void main(){
|
|
|
366
448
|
col = surfaceStreaks(vUv, col, crease);
|
|
367
449
|
col = applyColorGrade(col);
|
|
368
450
|
|
|
451
|
+
#ifdef POINTER_FX
|
|
452
|
+
// Local hue rotation + brightness lift near the cursor (both fade out with vPointerFall).
|
|
453
|
+
col = hueShift(col, radians(uPointerHue) * vPointerFall);
|
|
454
|
+
col *= 1.0 + uPointerLighten * vPointerFall;
|
|
455
|
+
#endif
|
|
456
|
+
|
|
369
457
|
// Iridescence: a thin-film / holographic hue that shifts with view angle. Reuses the same
|
|
370
458
|
// camera-facing ratio as roundness (recomputed here, since roundness may be off): grazing parts
|
|
371
459
|
// of the ribbon (low facing) shift hue most, so the colour flows as the ribbon curves. Skipped
|
|
@@ -428,6 +516,9 @@ void main(){
|
|
|
428
516
|
float ribEdge = smoothstep(0.0, 0.1, vUv.y) * (1.0 - smoothstep(0.9, 1.0, vUv.y));
|
|
429
517
|
#endif
|
|
430
518
|
float alpha = uOpacity * ribEdge;
|
|
519
|
+
#ifdef POINTER_FX
|
|
520
|
+
alpha *= clamp(1.0 - uPointerThin * vPointerFall, 0.0, 1.0); // solid: local translucency
|
|
521
|
+
#endif
|
|
431
522
|
if (uEdgeFade > 0.001) {
|
|
432
523
|
vec2 sc = gl_FragCoord.xy / max(uResolution, vec2(1.0));
|
|
433
524
|
float vig =
|
|
@@ -466,6 +557,12 @@ uniform vec3 uClearColor; // = page background colour (shown between t
|
|
|
466
557
|
|
|
467
558
|
varying vec2 vUv;
|
|
468
559
|
varying vec4 vClipPosition;
|
|
560
|
+
#ifdef POINTER_FX
|
|
561
|
+
uniform float uPointerThin; // 0..1 — strands taper to hairlines near the cursor
|
|
562
|
+
uniform float uPointerHue; // degrees, local hue rotation near the cursor
|
|
563
|
+
uniform float uPointerLighten; // -1..1 local brightness lift near the cursor
|
|
564
|
+
varying float vPointerFall; // falloff × presence, written by the vertex shader
|
|
565
|
+
#endif
|
|
469
566
|
|
|
470
567
|
${colorFns}
|
|
471
568
|
|
|
@@ -473,9 +570,17 @@ void main(){
|
|
|
473
570
|
// Same 2D palette sample + colour ops as the solid theme.
|
|
474
571
|
vec3 color = applyColorGrade(waveBaseColor(vUv));
|
|
475
572
|
|
|
573
|
+
#ifdef POINTER_FX
|
|
574
|
+
color = hueShift(color, radians(uPointerHue) * vPointerFall);
|
|
575
|
+
color *= 1.0 + uPointerLighten * vPointerFall;
|
|
576
|
+
#endif
|
|
577
|
+
|
|
476
578
|
// Carve into fine vertical lines; thickness from the screen-space uv derivative.
|
|
477
579
|
vec2 dy = dFdy(vUv);
|
|
478
580
|
float lineThickness = uLineThickness * pow(abs(dy.x * uMaxWidth), uLineDerivativePower);
|
|
581
|
+
#ifdef POINTER_FX
|
|
582
|
+
lineThickness *= clamp(1.0 - uPointerThin * vPointerFall, 0.0, 1.0); // wireframe: taper strands
|
|
583
|
+
#endif
|
|
479
584
|
float a = abs(sin(vUv.x * uLineAmount));
|
|
480
585
|
a = smoothstep(lineThickness, 0.0, a);
|
|
481
586
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shaders.js","names":[],"sources":["../../src/renderer/shaders.ts"],"sourcesContent":["import { MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS } from \"../config/model\";\n\n/**\n * The wave shaders. Vertex: a flat plane is Y-displaced by simplex noise, then\n * twisted by three axis-rotations `freq * expStep(uv, power)` where\n * `expStep(x,n) = exp2(-exp2(n)*pow(x,n))` is a falloff (rotation concentrated at\n * the uv=0 edge), with diagonal axes + an animated X wobble. Fragment: uses NO\n * normal-based lighting — \"thickness\" comes from `crease`, a foreshorten/fold\n * detector built from `dFdy(uv)`, used to lift flat areas toward white\n * (`col += (1-crease)*0.25`) and to localise the striations. Striations are subtle\n * high-frequency simplex noise ADDED to the colour, colour-matched via (1-blue)\n * and end-weighted via a parabola — so they blend rather than form hard lines.\n * Our additions: gradient stops/types for colour, and an optional additive light\n * layer (kept gentle so the default look is preserved).\n */\n\n// Noise function: xxHash-seeded unit-vector gradients + a Gustavson simplex. It uses\n// GLSL ES 3.00 integer ops (floatBitsToUint, unsigned bit-shifts) — available with no\n// glslVersion change because three compiles non-raw ShaderMaterials as \"#version 300 es\"\n// already. `hash` returns a vec2 here — the cheap grain hash in the fragment is named\n// `grainHash` to avoid clashing with it.\nconst simplex2d = /* glsl */ `\nfloat xxhash(vec2 x){\n uvec2 t = floatBitsToUint(x);\n uint h = 0xc2b2ae3du * t.x + 0x165667b9u;\n h = (h << 17u | h >> 15u) * 0x27d4eb2fu;\n h += 0xc2b2ae3du * t.y;\n h = (h << 17u | h >> 15u) * 0x27d4eb2fu;\n h ^= h >> 15u;\n h *= 0x85ebca77u;\n h ^= h >> 13u;\n h *= 0xc2b2ae3du;\n h ^= h >> 16u;\n return uintBitsToFloat(h >> 9u | 0x3f800000u) - 1.0;\n}\nvec2 hash(vec2 x){\n float k = 6.283185307 * xxhash(x);\n return vec2(cos(k), sin(k));\n}\nfloat simplexNoise(in vec2 p){\n const float K1 = 0.366025404; // (sqrt(3)-1)/2\n const float K2 = 0.211324865; // (3-sqrt(3))/6\n vec2 i = floor(p + (p.x + p.y) * K1);\n vec2 a = p - i + (i.x + i.y) * K2;\n float m = step(a.y, a.x);\n vec2 o = vec2(m, 1.0 - m);\n vec2 b = a - o + K2;\n vec2 c = a - 1.0 + 2.0 * K2;\n vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);\n vec3 n = h * h * h * vec3(dot(a, hash(i + 0.0)), dot(b, hash(i + o)), dot(c, hash(i + 1.0)));\n return dot(n, vec3(32.99)); // analytic factor (= 2916*sqrt(2)/125)\n}\n`;\n\n// Uniforms shared by BOTH fragment shaders (solid + wireframe line): the palette/gradient\n// inputs and the colour-grade knobs. Each shader declares its theme-specific uniforms beside\n// this block. Requires MAX_COLORS / MAX_MESH_POINTS #defines.\nconst colorUniforms = /* glsl */ `\nuniform vec3 uColors[MAX_COLORS];\nuniform float uColorPos[MAX_COLORS];\nuniform int uColorCount;\nuniform int uGradType;\nuniform float uGradAngle;\nuniform float uGradShift;\nuniform vec2 uMeshPointPos[MAX_MESH_POINTS];\nuniform vec3 uMeshPointColor[MAX_MESH_POINTS];\nuniform float uMeshPointInfluence[MAX_MESH_POINTS];\nuniform int uMeshPointCount;\nuniform float uMeshSoftness;\nuniform sampler2D uPalette; // baked 2D palette texture\nuniform float uUsePalette; // >0.5 = sample the texture; else procedural grad()\nuniform float uPaletteRaw; // >0.5 = sample palette by raw (uv.x,uv.y), not gradCoord\nuniform vec2 uPaletteScale;\nuniform vec2 uPaletteOffset;\nuniform float uPaletteRotation;\nuniform float uHueShift;\nuniform float uContrast;\nuniform float uSaturation;\nuniform float uOpacity;\nuniform float uSquared; // 1 = square the output colour (the deep \"squared\" hero look)\n`;\n\n// Colour helpers + the palette/gradient sampler shared by both fragment shaders.\n// Interpolate AFTER ${\"simplex2d\"} and ${\"colorUniforms\"} (gradCoord needs both) and a PI define.\nconst colorFns = /* glsl */ `\nvec3 contrastFn(vec3 v, float a){ return (v - 0.5) * a + 0.5; }\nvec3 desaturate(vec3 color, float factor){\n vec3 gray = vec3(dot(vec3(0.299, 0.587, 0.114), color));\n return mix(color, gray, factor);\n}\nvec3 hueShift(vec3 color, float shift){\n vec3 g = vec3(0.57735);\n vec3 proj = g * dot(g, color);\n vec3 U = color - proj;\n vec3 W = cross(g, U);\n return U * cos(shift) + W * sin(shift) + proj;\n}\n\n// Our gradient: interpolate stops by their positions (uColorPos sorted ascending).\nvec3 grad(float u){\n u = clamp(u, 0.0, 1.0);\n vec3 col = uColors[0];\n for (int i = 0; i < MAX_COLORS - 1; i++){\n if (i >= uColorCount - 1) break;\n float p0 = uColorPos[i];\n float p1 = uColorPos[i + 1];\n if (u >= p0){\n float t = clamp((u - p0) / max(p1 - p0, 1e-5), 0.0, 1.0);\n col = mix(uColors[i], uColors[i + 1], t);\n }\n }\n return col;\n}\n\n// iOS-style 2D colour field. Each control point contributes an inverse-distance\n// weight; normalising the sum fills the whole surface without dark seams.\nvec3 meshGradient(vec2 uv){\n vec3 colorSum = vec3(0.0);\n float weightSum = 0.0;\n float exponent = mix(4.8, 1.35, clamp(uMeshSoftness, 0.0, 1.0));\n for (int i = 0; i < MAX_MESH_POINTS; i++){\n if (i >= uMeshPointCount) break;\n float influence = max(uMeshPointInfluence[i], 0.05);\n float distanceFromPoint = length(uv - uMeshPointPos[i]) / influence;\n float weight = 1.0 / (pow(max(distanceFromPoint, 0.012), exponent) + 0.002);\n colorSum += uMeshPointColor[i] * weight;\n weightSum += weight;\n }\n return colorSum / max(weightSum, 0.0001);\n}\n\n// Map a surface uv to the 0–1 gradient coordinate per gradient type. uGradShift\n// adds a low-frequency simplex warp so the colour varies in 2D (along the length\n// as well as across the width) — a 2D palette feel instead\n// of flat 1-D bands.\nfloat gradCoord(vec2 uv){\n float warp = uGradShift * simplexNoise(uv * 1.6 + 4.0);\n if (uGradType == 1){ return clamp(length(uv - 0.5) * 2.0 + warp, 0.0, 1.0); } // radial\n if (uGradType == 2){ return fract(atan(uv.y - 0.5, uv.x - 0.5) / (2.0 * PI) + 0.5 + warp); } // conic\n vec2 dir = vec2(sin(uGradAngle), cos(uGradAngle)); // linear, angled\n return clamp(dot(uv - 0.5, dir) + 0.5 + warp, 0.0, 1.0);\n}\n\n// One base-colour sample for the whole surface: rotate/scale/offset the raw-palette uv,\n// then pick the mesh field / baked 2D texture / procedural stops by mode. The raw palette\n// is sampled by (uv.x, uv.y) directly; the stops-generated texture is sampled via\n// gradCoord so its angle/type/warp still apply.\nvec3 waveBaseColor(vec2 uv){\n float gc = gradCoord(uv);\n vec2 mediaUv = uv - 0.5;\n float mediaCos = cos(uPaletteRotation);\n float mediaSin = sin(uPaletteRotation);\n mediaUv = vec2(\n mediaCos * mediaUv.x + mediaSin * mediaUv.y,\n -mediaSin * mediaUv.x + mediaCos * mediaUv.y\n );\n mediaUv = mediaUv * uPaletteScale + 0.5 + uPaletteOffset;\n vec2 puv = uPaletteRaw > 0.5\n ? clamp(mediaUv, 0.0, 1.0)\n : vec2(gc, clamp(uv.y, 0.0, 1.0));\n return uGradType == 3\n ? meshGradient(uv)\n : (uUsePalette > 0.5 ? texture2D(uPalette, puv).rgb : grad(gc));\n}\n\n// The shared colour grade: contrast → desaturate → hue rotate (degrees).\nvec3 applyColorGrade(vec3 c){\n c = contrastFn(c, uContrast);\n c = desaturate(c, 1.0 - uSaturation);\n return hueShift(c, radians(uHueShift));\n}\n`;\n\nexport const vertexShader = /* glsl */ `\n${simplex2d}\n\nuniform float uTime, uSpeed, uSeed;\nuniform float uDispFreqX, uDispFreqZ, uDispAmount;\nuniform float uDetailFreq, uDetailAmount; // 2nd displacement octave (only read under DETAIL_OCTAVE)\nuniform float uTwFreqX, uTwFreqY, uTwFreqZ, uTwPowX, uTwPowY, uTwPowZ;\nuniform float uLoopSeconds; // seamless-loop period (only read under LOOP_MOTION)\n\nvarying vec2 vUv;\nvarying vec3 vWorldPos;\nvarying vec3 vViewDir;\nvarying vec4 vClipPosition; // = gl_Position, for the wireframe theme's depth fade\n\n// expStep: a falloff from 1 (at x=0) toward 0, sharpness set by n. The\n// max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n\n// just concentrates the twist toward the OTHER end instead.\nfloat expStep(float x, float n){ return exp2(-exp2(n) * pow(max(x, 1.0e-3), n)); }\n\n// rotationMatrix (mat4), used row-vector style: pos = (vec4(pos,1) * R).xyz\nmat4 rotationMatrix(vec3 axis, float angle){\n axis = normalize(axis);\n float s = sin(angle), c = cos(angle), oc = 1.0 - c;\n return mat4(\n oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s, 0.0,\n oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s, 0.0,\n oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c, 0.0,\n 0.0, 0.0, 0.0, 1.0\n );\n}\n\nvoid main(){\n vUv = uv;\n#ifndef LOOP_MOTION\n float t = uTime * uSpeed + uSeed;\n#endif\n\n#ifdef LOOP_MOTION\n // Seamless loop: rather than scrolling the noise field linearly by t (which never repeats),\n // sample it on a circle of radius loopR at angle loopTheta — exactly periodic with period\n // uLoopSeconds. The tangential speed loopR·dθ/dt equals uSpeed, so the looped motion advances\n // at the same rate as the linear drift, just curved into a closed orbit (it orbits rather than\n // drifts — the trade-off for a seamless loop, hence opt-in). uSeed offsets the phase so stacked\n // waves keep their relative motion while sharing the single period.\n float loopTheta = uTime * (6.28318530718 / uLoopSeconds) + uSeed;\n float loopR = uSpeed * uLoopSeconds * 0.159154943092; // = uSpeed·uLoopSeconds / (2π)\n vec2 loopOff = loopR * vec2(cos(loopTheta), sin(loopTheta));\n#endif\n\n // The base geometry is already a baked hairpin fold. On top of it we deform the\n // vertices: a displacement lifts Y by simplex noise of the (x,z) position, then\n // three axis-rotations twist the strip.\n vec3 pos = position;\n#ifdef LOOP_MOTION\n pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX, pos.z * uDispFreqZ) + loopOff);\n#else\n pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX + t, pos.z * uDispFreqZ + t));\n#endif\n#ifdef DETAIL_OCTAVE\n // A second, finer octave riding on the broad swell — fine ripples on top of the big shape, a\n // shape vocabulary single-octave displacement can't reach. Shares the loop orbit so it stays\n // periodic when looping.\n#ifdef LOOP_MOTION\n pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq, pos.z * uDetailFreq) + loopOff);\n#else\n pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq + t, pos.z * uDetailFreq + t));\n#endif\n#endif\n\n // The X-twist frequency feeding rotB. Two modes: by default uTwFreqX is used\n // directly; the variant (used by the Wave 4 preset) modulates it with\n // simplex noise indexed along the ribbon (uv.y) so the twist breathes over time.\n // We gate the wobble with a #define so the compiled program is unchanged when off.\n float twistXFreq = uTwFreqX;\n#ifdef TWIST_MOTION\n#ifdef LOOP_MOTION\n float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, 0.0) + loopOff);\n#else\n float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, t));\n#endif\n twistXFreq = uTwFreqX - twistXNoise * 0.1;\n#endif\n\n // Three-axis twist: expStep falloff sets how\n // sharply each rotation concentrates toward an edge. rotA keys off uv.x, rotB/rotC\n // off uv.y; axes (0.5,0,0.5) and (0,0.5,0.5) are normalised inside rotationMatrix.\n mat4 rotA = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqY * expStep(uv.x, uTwPowY));\n mat4 rotB = rotationMatrix(vec3(0.0, 0.5, 0.5), twistXFreq * expStep(uv.y, uTwPowX));\n mat4 rotC = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqZ * expStep(uv.y, uTwPowZ));\n pos = (vec4(pos, 1.0) * rotA).xyz;\n pos = (vec4(pos, 1.0) * rotB).xyz;\n pos = (vec4(pos, 1.0) * rotC).xyz;\n\n // The scale / rotation / position transform lives on the mesh (modelMatrix), so the\n // orientation matches THREE's Euler-XYZ rather than an in-shader rotation order.\n vec4 world = modelMatrix * vec4(pos, 1.0);\n vWorldPos = world.xyz;\n vViewDir = cameraPosition - world.xyz;\n gl_Position = projectionMatrix * viewMatrix * world;\n vClipPosition = gl_Position;\n}\n`;\n\nexport const fragmentShader = /* glsl */ `\n#define MAX_COLORS ${MAX_COLORS}\n#define MAX_MESH_POINTS ${MAX_MESH_POINTS}\n#define MAX_LIGHTS ${MAX_LIGHTS}\n#define MAX_NOISE_BANDS ${MAX_NOISE_BANDS}\n#define PI 3.14159265359\n\n${simplex2d}\n\n${colorUniforms}\nuniform float uDebug; // dev: 1 = show crease, 2 = show derivative normal\nuniform float uSheen; // white-lift on the flat (low-crease) areas (1 = full)\nuniform float uRoundness; // pose-robust normal-based roundness/thickness strength\nuniform float uIridescence; // thin-film hue shift with view angle (0 = off)\nuniform float uFiberCount;\nuniform float uFiberStrength;\nuniform float uTexture;\nuniform float uCreaseLight;\nuniform float uCreaseSharpness;\nuniform float uCreaseSoftness;\nuniform float uEdgeFade;\nuniform vec2 uResolution;\nuniform float uAmbient;\nuniform int uNumLights;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightIntensity[MAX_LIGHTS];\nuniform int uNumNoiseBands;\nuniform vec4 uNoiseBandBounds[MAX_NOISE_BANDS]; // (startX, endX, startY, endY)\nuniform vec4 uNoiseBandParams[MAX_NOISE_BANDS]; // (feather, strength, frequency, colorAttenuation)\nuniform float uNoiseBandParaPow[MAX_NOISE_BANDS];\n\nvarying vec2 vUv;\nvarying vec3 vWorldPos;\nvarying vec3 vViewDir;\n#ifdef DEPTH_TINT\nuniform float uDepthTint;\nuniform vec3 uDepthTintColor;\nvarying vec4 vClipPosition; // clip-space depth (written by the vertex shader for both programs)\n#endif\n#ifdef EDGE_FEATHER\nuniform float uEdgeFeather; // ribbon long-edge softness (only when it differs from the 0.1 default)\n#endif\n\n// Cheap value hash for the optional grain overlay (distinct from the simplex hash).\nfloat grainHash(vec2 p){ return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\n\nfloat parabola(float x, float k){ return pow(4.0 * x * (1.0 - x), k); }\nfloat mapLinear(float v, float a, float b, float c, float d){ return c + (v - a) * (d - c) / (b - a); }\n\n${colorFns}\n\n// Striations: a subtle high-frequency simplex-noise grain ADDED to the\n// colour — colour-matched (weaker where blue is high), only near folds (crease), and\n// concentrated toward the ends (parabola). Blends in rather than reading as hard lines.\nvec3 surfaceStreaks(vec2 uv, vec3 color, float crease){\n float strength = uFiberStrength; // default 0.2\n float freq = uFiberCount; // default 600\n float colorAtten = 0.9;\n float paraPow = 3.0;\n // Noise bands: inside each rectangular uv region the\n // fiber params are overridden, so the streaks vary per region instead of uniform.\n for (int i = 0; i < MAX_NOISE_BANDS; i++) {\n if (i >= uNumNoiseBands) break;\n vec4 b = uNoiseBandBounds[i];\n vec4 prm = uNoiseBandParams[i];\n float feather = max(prm.x, 1.0e-4);\n float blend =\n smoothstep(b.x - feather, b.x, uv.x) * (1.0 - smoothstep(b.y, b.y + feather, uv.x)) *\n smoothstep(b.z - feather, b.z, uv.y) * (1.0 - smoothstep(b.w, b.w + feather, uv.y));\n strength = mix(strength, prm.y, blend);\n freq = mix(freq, prm.z, blend);\n colorAtten = mix(colorAtten, prm.w, blend);\n paraPow = mix(paraPow, uNoiseBandParaPow[i], blend);\n }\n // The high frequency runs along uv.x (the ribbon's length) so the streaks read as\n // fine lengthwise fibers; end-weighted by 1 - parabola(uv.x).\n float p = 1.0 - parabola(uv.x, paraPow);\n float n0 = simplexNoise(vec2(uv.x * 0.1, uv.y * 0.5));\n float n1 = simplexNoise(vec2(uv.x * (freq + freq * 0.5 * n0), uv.y * 4.0 * n0));\n n1 = mapLinear(n1, -1.0, 1.0, 0.0, 1.0);\n color += n1 * strength * (1.0 - color.b * colorAtten) * crease * p;\n return color;\n}\n\nvoid main(){\n // crease: a foreshortening / fold detector from the screen-space uv derivative.\n // It drives BOTH the roundness shading and where the streaks appear — this is what\n // gives the wave its thickness without any normal-based lighting.\n float crease = dFdy(vUv).y * uResolution.y * uCreaseLight;\n crease = clamp(mapLinear(crease, -1.0, 1.0, 0.0, 1.0), 0.0, 1.0);\n crease = pow(crease, uCreaseSharpness);\n crease = clamp(smoothstep(0.0, uCreaseSoftness, crease), 0.0, 1.0);\n\n // Debug visualisations (dev): 1 = crease value, 2 = derivative surface normal.\n if (uDebug > 0.5) {\n if (uDebug < 1.5) { gl_FragColor = vec4(vec3(crease), 1.0); return; }\n vec3 dn = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n gl_FragColor = vec4(dn * 0.5 + 0.5, 1.0); return;\n }\n\n // Colour: sample the baked 2D palette texture, or fall back to the procedural 1-D\n // gradient (see waveBaseColor).\n vec3 col = waveBaseColor(vUv);\n col = surfaceStreaks(vUv, col, crease);\n col = applyColorGrade(col);\n\n // Iridescence: a thin-film / holographic hue that shifts with view angle. Reuses the same\n // camera-facing ratio as roundness (recomputed here, since roundness may be off): grazing parts\n // of the ribbon (low facing) shift hue most, so the colour flows as the ribbon curves. Skipped\n // at 0, so the compiled result is unchanged when off.\n if (uIridescence > 0.001) {\n vec3 iridN = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n float iridFacing = abs(dot(iridN, normalize(vViewDir)));\n col = hueShift(col, (1.0 - iridFacing) * uIridescence * PI);\n }\n\n // Sheen: lift the flat (low-crease) areas toward white. This is\n // pose-dependent (it keys off dFdy(uv.y)), so we keep it gentle and add a robust term.\n col += (1.0 - crease) * 0.25 * uSheen;\n\n // Pose-robust roundness: shade by the camera-facing ratio of the derivative surface\n // normal so the ribbon reads as a rounded, grabbable solid from any angle. Grazing\n // edges darken into shadow (defining the rounded form), the body keeps its full colour,\n // and the most face-on sliver catches a soft highlight. uRoundness = strength.\n if (uRoundness > 0.001) {\n vec3 volN = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n float facing = abs(dot(volN, normalize(vViewDir))); // 1 = facing camera, 0 = edge-on\n col *= mix(1.0 - 0.6 * uRoundness, 1.0, facing); // deepen grazing edges → solid form\n col += smoothstep(0.65, 1.0, facing) * uRoundness * 0.18; // soft highlight on the facing body\n }\n\n // Optional positionable lights (our feature) — additive & gentle, on top of the\n // base shading so the default look is preserved. A finely-subdivided mesh\n // keeps this derivative normal smooth.\n if (uNumLights > 0) {\n vec3 N = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n vec3 Vd = normalize(vViewDir);\n if (dot(N, Vd) < 0.0) N = -N;\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uNumLights) break;\n vec3 L = normalize(uLightPos[i] - vWorldPos);\n vec3 lc = uLightColor[i] * uLightIntensity[i];\n float diff = max(dot(N, L), 0.0);\n float spec = pow(max(dot(N, normalize(L + Vd)), 0.0), 28.0);\n col += col * diff * lc * 0.16 + spec * lc * 0.10;\n }\n }\n col *= 0.55 + clamp(uAmbient, 0.0, 1.0); // overall level; default 0.45 => x1.0 (neutral)\n\n#ifdef DEPTH_TINT\n // Depth tint: fade far fragments toward a colour so a multi-wave stack gains atmospheric\n // separation — near strands keep their colour, far ones recede. Reuses the clip-space depth the\n // wireframe theme fades with (clamp(z*6), where 1 = far).\n col = mix(col, uDepthTintColor, clamp(vClipPosition.z * 6.0, 0.0, 1.0) * uDepthTint);\n#endif\n\n if (uTexture > 0.001) col *= 1.0 + (grainHash(vUv * 850.0) - 0.5) * uTexture * 0.25;\n\n // Soft long edges + optional viewport-edge fade. The edge softness is the hardcoded 0.1 by\n // default (literal branch → byte-identical); EDGE_FEATHER swaps in the uEdgeFeather knob only\n // when it differs, so razor-crisp or vapor-soft edges are both reachable.\n#ifdef EDGE_FEATHER\n float ribEdge =\n smoothstep(0.0, uEdgeFeather, vUv.y) * (1.0 - smoothstep(1.0 - uEdgeFeather, 1.0, vUv.y));\n#else\n float ribEdge = smoothstep(0.0, 0.1, vUv.y) * (1.0 - smoothstep(0.9, 1.0, vUv.y));\n#endif\n float alpha = uOpacity * ribEdge;\n if (uEdgeFade > 0.001) {\n vec2 sc = gl_FragCoord.xy / max(uResolution, vec2(1.0));\n float vig =\n smoothstep(0.0, uEdgeFade, sc.x) * (1.0 - smoothstep(1.0 - uEdgeFade, 1.0, sc.x)) *\n smoothstep(0.0, uEdgeFade, sc.y) * (1.0 - smoothstep(1.0 - uEdgeFade, 1.0, sc.y));\n alpha *= vig;\n }\n\n // Deep \"squared\" hero colour: formerly done by a framebuffer-squaring blend that REPLACED the\n // destination (punching holes at soft edges / where waves overlap). Squaring here + normal\n // premultiplied compositing (see applyBlendMode) keeps the deep colour and blends correctly.\n col = clamp(col, 0.0, 1.0);\n // Square colour AND alpha so the soft ribbon edges keep the crisp, thin feather of the original\n // squared-blend look — but now composited (premultiplied) rather than replace-blended, so they\n // no longer punch holes. Over an opaque background alpha² still resolves to fully opaque.\n if (uSquared > 0.5) { col *= col; alpha *= alpha; }\n gl_FragColor = vec4(col, alpha);\n#ifdef PREMULTIPLIED_ALPHA\n gl_FragColor.rgb *= gl_FragColor.a;\n#endif\n}\n`;\n\n// ---- Wireframe \"thin-line\" theme ----\n// The same wave geometry, but instead of a solid surface the colour is carved into fine\n// vertical lines (abs(sin(uv.x * lineAmount))) whose thickness scales with the screen-\n// space uv derivative, then mixed line<->background with a depth fade. Used by the dark\n// hero preset. hueShift takes degrees (radians() here) to match the light shader.\nexport const lineFragmentShader = /* glsl */ `\n#define MAX_COLORS ${MAX_COLORS}\n#define MAX_MESH_POINTS ${MAX_MESH_POINTS}\n#define PI 3.14159265359\n\n${simplex2d}\n\n${colorUniforms}\nuniform float uLineAmount; // default 425\nuniform float uLineThickness; // default 1\nuniform float uLineDerivativePower; // default 0.95\nuniform float uMaxWidth; // default 1232\nuniform vec3 uClearColor; // = page background colour (shown between the lines)\n\nvarying vec2 vUv;\nvarying vec4 vClipPosition;\n\n${colorFns}\n\nvoid main(){\n // Same 2D palette sample + colour ops as the solid theme.\n vec3 color = applyColorGrade(waveBaseColor(vUv));\n\n // Carve into fine vertical lines; thickness from the screen-space uv derivative.\n vec2 dy = dFdy(vUv);\n float lineThickness = uLineThickness * pow(abs(dy.x * uMaxWidth), uLineDerivativePower);\n float a = abs(sin(vUv.x * uLineAmount));\n a = smoothstep(lineThickness, 0.0, a);\n\n // Depth fade: the wave recedes into the background colour with depth. Watch the\n // argument order: clamp(0.0, 1.0, z*6) is a swapped-args trap — it clamps the\n // constant 0.0 into [1.0, z*6], i.e. min(1.0, z*6), which (with our ortho clip.z\n // range) collapses the whole wave to the background. The correct clamp(z*6, 0, 1)\n // gives the proper subtle far-end fade and thin-line look.\n float depthFade = clamp(vClipPosition.z * 6.0, 0.0, 1.0);\n color = mix(uClearColor, color, a * (1.0 - depthFade));\n if (uSquared > 0.5) color *= color; // deep \"squared\" look, now composited not replace-blended\n gl_FragColor = vec4(color, uOpacity);\n#ifdef PREMULTIPLIED_ALPHA\n gl_FragColor.rgb *= gl_FragColor.a;\n#endif\n}\n`;\n\n// ---- Post pass: viewport-edge soft-focus blur + dither grain ----\n\nexport const postVertexShader = /* glsl */ `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nexport const postFragmentShader = /* glsl */ `\nuniform sampler2D tDiffuse;\nuniform vec2 uResolution;\nuniform float uBlurAmount;\nuniform int uBlurSamples;\nuniform float uGrainAmount;\nuniform float uTime;\nvarying vec2 vUv;\n\nfloat random2(vec2 st){ return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453); }\n\n// Angular (spin) blur: rotate the sample coord around the centre and\n// accumulate — a tangential smear that grows toward the edges. Carries alpha so a\n// transparent background survives the post pass.\nvec4 blurAngular(sampler2D tex, vec2 uv, float angle, int samples){\n vec4 total = vec4(0.0);\n vec2 coord = uv - 0.5;\n float dist = 1.0 / float(samples);\n vec2 dir = vec2(cos(angle * dist), sin(angle * dist));\n mat2 rot = mat2(dir.x, dir.y, -dir.y, dir.x);\n for (int i = 0; i < 64; i++){\n if (i >= samples) break;\n total += texture2D(tex, coord + 0.5);\n coord = coord * rot; // row-vector order (coord * rot) sets the spin direction\n }\n return total * dist;\n}\n\nvoid main(){\n vec4 sceneColor = texture2D(tDiffuse, vUv);\n vec4 blurColor = blurAngular(tDiffuse, vUv, uBlurAmount, uBlurSamples);\n // blurPower: keep a sharp band weighted to the middle, blurring toward top & bottom.\n float blurPower = smoothstep(0.0, 0.7, vUv.y) - smoothstep(0.2, 1.0, vUv.y);\n vec4 color = mix(blurColor, sceneColor, blurPower);\n // Static film grain: keyed off gl_FragCoord only (no uTime), so it doesn't flicker.\n color.rgb += mix(uGrainAmount, -uGrainAmount, random2(gl_FragCoord.xy * 0.01)) * (4.0 / 255.0);\n gl_FragColor = color; // preserve alpha → transparent background works\n}\n`;\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,MAAM,YAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoC7B,MAAM,gBAA2B;;;;;;;;;;;;;;;;;;;;;;;;AA2BjC,MAAM,WAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyF5B,MAAa,eAA0B;EACrC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsGZ,MAAa,iBAA4B;;;;;;;EAOvC,UAAU;;EAEV,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyCd,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmJX,MAAa,qBAAgC;;;;;EAK3C,UAAU;;EAEV,cAAc;;;;;;;;;;EAUd,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BX,MAAa,mBAA8B;;;;;;;AAQ3C,MAAa,qBAAgC"}
|
|
1
|
+
{"version":3,"file":"shaders.js","names":[],"sources":["../../src/renderer/shaders.ts"],"sourcesContent":["import { MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS } from \"../config/model\";\n\n/**\n * The wave shaders. Vertex: a flat plane is Y-displaced by simplex noise, then\n * twisted by three axis-rotations `freq * expStep(uv, power)` where\n * `expStep(x,n) = exp2(-exp2(n)*pow(x,n))` is a falloff (rotation concentrated at\n * the uv=0 edge), with diagonal axes + an animated X wobble. Fragment: uses NO\n * normal-based lighting — \"thickness\" comes from `crease`, a foreshorten/fold\n * detector built from `dFdy(uv)`, used to lift flat areas toward white\n * (`col += (1-crease)*0.25`) and to localise the striations. Striations are subtle\n * high-frequency simplex noise ADDED to the colour, colour-matched via (1-blue)\n * and end-weighted via a parabola — so they blend rather than form hard lines.\n * Our additions: gradient stops/types for colour, and an optional additive light\n * layer (kept gentle so the default look is preserved).\n */\n\n// Noise function: xxHash-seeded unit-vector gradients + a Gustavson simplex. It uses\n// GLSL ES 3.00 integer ops (floatBitsToUint, unsigned bit-shifts) — available with no\n// glslVersion change because three compiles non-raw ShaderMaterials as \"#version 300 es\"\n// already. `hash` returns a vec2 here — the cheap grain hash in the fragment is named\n// `grainHash` to avoid clashing with it.\nconst simplex2d = /* glsl */ `\nfloat xxhash(vec2 x){\n uvec2 t = floatBitsToUint(x);\n uint h = 0xc2b2ae3du * t.x + 0x165667b9u;\n h = (h << 17u | h >> 15u) * 0x27d4eb2fu;\n h += 0xc2b2ae3du * t.y;\n h = (h << 17u | h >> 15u) * 0x27d4eb2fu;\n h ^= h >> 15u;\n h *= 0x85ebca77u;\n h ^= h >> 13u;\n h *= 0xc2b2ae3du;\n h ^= h >> 16u;\n return uintBitsToFloat(h >> 9u | 0x3f800000u) - 1.0;\n}\nvec2 hash(vec2 x){\n float k = 6.283185307 * xxhash(x);\n return vec2(cos(k), sin(k));\n}\nfloat simplexNoise(in vec2 p){\n const float K1 = 0.366025404; // (sqrt(3)-1)/2\n const float K2 = 0.211324865; // (3-sqrt(3))/6\n vec2 i = floor(p + (p.x + p.y) * K1);\n vec2 a = p - i + (i.x + i.y) * K2;\n float m = step(a.y, a.x);\n vec2 o = vec2(m, 1.0 - m);\n vec2 b = a - o + K2;\n vec2 c = a - 1.0 + 2.0 * K2;\n vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);\n vec3 n = h * h * h * vec3(dot(a, hash(i + 0.0)), dot(b, hash(i + o)), dot(c, hash(i + 1.0)));\n return dot(n, vec3(32.99)); // analytic factor (= 2916*sqrt(2)/125)\n}\n`;\n\n// Uniforms shared by BOTH fragment shaders (solid + wireframe line): the palette/gradient\n// inputs and the colour-grade knobs. Each shader declares its theme-specific uniforms beside\n// this block. Requires MAX_COLORS / MAX_MESH_POINTS #defines.\nconst colorUniforms = /* glsl */ `\nuniform vec3 uColors[MAX_COLORS];\nuniform float uColorPos[MAX_COLORS];\nuniform int uColorCount;\nuniform int uGradType;\nuniform float uGradAngle;\nuniform float uGradShift;\nuniform vec2 uMeshPointPos[MAX_MESH_POINTS];\nuniform vec3 uMeshPointColor[MAX_MESH_POINTS];\nuniform float uMeshPointInfluence[MAX_MESH_POINTS];\nuniform int uMeshPointCount;\nuniform float uMeshSoftness;\nuniform sampler2D uPalette; // baked 2D palette texture\nuniform float uUsePalette; // >0.5 = sample the texture; else procedural grad()\nuniform float uPaletteRaw; // >0.5 = sample palette by raw (uv.x,uv.y), not gradCoord\nuniform vec2 uPaletteScale;\nuniform vec2 uPaletteOffset;\nuniform float uPaletteRotation;\nuniform float uHueShift;\nuniform float uContrast;\nuniform float uSaturation;\nuniform float uOpacity;\nuniform float uSquared; // 1 = square the output colour (the deep \"squared\" hero look)\n`;\n\n// Colour helpers + the palette/gradient sampler shared by both fragment shaders.\n// Interpolate AFTER ${\"simplex2d\"} and ${\"colorUniforms\"} (gradCoord needs both) and a PI define.\nconst colorFns = /* glsl */ `\nvec3 contrastFn(vec3 v, float a){ return (v - 0.5) * a + 0.5; }\nvec3 desaturate(vec3 color, float factor){\n vec3 gray = vec3(dot(vec3(0.299, 0.587, 0.114), color));\n return mix(color, gray, factor);\n}\nvec3 hueShift(vec3 color, float shift){\n vec3 g = vec3(0.57735);\n vec3 proj = g * dot(g, color);\n vec3 U = color - proj;\n vec3 W = cross(g, U);\n return U * cos(shift) + W * sin(shift) + proj;\n}\n\n// Our gradient: interpolate stops by their positions (uColorPos sorted ascending).\nvec3 grad(float u){\n u = clamp(u, 0.0, 1.0);\n vec3 col = uColors[0];\n for (int i = 0; i < MAX_COLORS - 1; i++){\n if (i >= uColorCount - 1) break;\n float p0 = uColorPos[i];\n float p1 = uColorPos[i + 1];\n if (u >= p0){\n float t = clamp((u - p0) / max(p1 - p0, 1e-5), 0.0, 1.0);\n col = mix(uColors[i], uColors[i + 1], t);\n }\n }\n return col;\n}\n\n// iOS-style 2D colour field. Each control point contributes an inverse-distance\n// weight; normalising the sum fills the whole surface without dark seams.\nvec3 meshGradient(vec2 uv){\n vec3 colorSum = vec3(0.0);\n float weightSum = 0.0;\n float exponent = mix(4.8, 1.35, clamp(uMeshSoftness, 0.0, 1.0));\n for (int i = 0; i < MAX_MESH_POINTS; i++){\n if (i >= uMeshPointCount) break;\n float influence = max(uMeshPointInfluence[i], 0.05);\n float distanceFromPoint = length(uv - uMeshPointPos[i]) / influence;\n float weight = 1.0 / (pow(max(distanceFromPoint, 0.012), exponent) + 0.002);\n colorSum += uMeshPointColor[i] * weight;\n weightSum += weight;\n }\n return colorSum / max(weightSum, 0.0001);\n}\n\n// Map a surface uv to the 0–1 gradient coordinate per gradient type. uGradShift\n// adds a low-frequency simplex warp so the colour varies in 2D (along the length\n// as well as across the width) — a 2D palette feel instead\n// of flat 1-D bands.\nfloat gradCoord(vec2 uv){\n float warp = uGradShift * simplexNoise(uv * 1.6 + 4.0);\n if (uGradType == 1){ return clamp(length(uv - 0.5) * 2.0 + warp, 0.0, 1.0); } // radial\n if (uGradType == 2){ return fract(atan(uv.y - 0.5, uv.x - 0.5) / (2.0 * PI) + 0.5 + warp); } // conic\n vec2 dir = vec2(sin(uGradAngle), cos(uGradAngle)); // linear, angled\n return clamp(dot(uv - 0.5, dir) + 0.5 + warp, 0.0, 1.0);\n}\n\n// One base-colour sample for the whole surface: rotate/scale/offset the raw-palette uv,\n// then pick the mesh field / baked 2D texture / procedural stops by mode. The raw palette\n// is sampled by (uv.x, uv.y) directly; the stops-generated texture is sampled via\n// gradCoord so its angle/type/warp still apply.\nvec3 waveBaseColor(vec2 uv){\n float gc = gradCoord(uv);\n vec2 mediaUv = uv - 0.5;\n float mediaCos = cos(uPaletteRotation);\n float mediaSin = sin(uPaletteRotation);\n mediaUv = vec2(\n mediaCos * mediaUv.x + mediaSin * mediaUv.y,\n -mediaSin * mediaUv.x + mediaCos * mediaUv.y\n );\n mediaUv = mediaUv * uPaletteScale + 0.5 + uPaletteOffset;\n vec2 puv = uPaletteRaw > 0.5\n ? clamp(mediaUv, 0.0, 1.0)\n : vec2(gc, clamp(uv.y, 0.0, 1.0));\n return uGradType == 3\n ? meshGradient(uv)\n : (uUsePalette > 0.5 ? texture2D(uPalette, puv).rgb : grad(gc));\n}\n\n// The shared colour grade: contrast → desaturate → hue rotate (degrees).\nvec3 applyColorGrade(vec3 c){\n c = contrastFn(c, uContrast);\n c = desaturate(c, 1.0 - uSaturation);\n return hueShift(c, radians(uHueShift));\n}\n`;\n\nexport const vertexShader = /* glsl */ `\n${simplex2d}\n\nuniform float uTime, uSpeed, uSeed;\nuniform float uDispFreqX, uDispFreqZ, uDispAmount;\nuniform float uDetailFreq, uDetailAmount; // 2nd displacement octave (only read under DETAIL_OCTAVE)\nuniform float uTwFreqX, uTwFreqY, uTwFreqZ, uTwPowX, uTwPowY, uTwPowZ;\nuniform float uLoopSeconds; // seamless-loop period (only read under LOOP_MOTION)\n\nvarying vec2 vUv;\nvarying vec3 vWorldPos;\nvarying vec3 vViewDir;\nvarying vec4 vClipPosition; // = gl_Position, for the wireframe theme's depth fade\n\n// Pointer field (optional, additive). ALL declarations here sit behind POINTER_FX so a wave with\n// no interaction config compiles the exact same program (JS-side uniform entries are always present\n// — see makeUniforms — but three only uploads uniforms the compiled program actually declares).\n#ifdef POINTER_FX\nuniform vec2 uPointer; // smoothed pointer, NDC (-1..1)\nuniform float uPointerActive; // presence ramp 0..1 × per-wave influence\nuniform float uPointerRadius; // falloff radius in NDC-y units (config radius × 2)\nuniform float uPointerAspect; // drawing-buffer dw/dh (circular screen falloff)\nuniform float uPointerAgitate;\nuniform float uPointerPush; // signed membrane dome at the cursor (+ repel / − attract)\nuniform float uPointerWake; // drag-wake trough amplitude (behind the moving cursor)\nuniform vec2 uPointerVel; // smoothed pointer velocity, NDC/s (drag-wake direction)\nvarying float vPointerFall; // falloff × presence — consumed by both fragment themes\n#ifdef POINTER_RIPPLES\nuniform vec2 uRippleOrigin[4]; // NDC\nuniform float uRippleAge[4]; // seconds since spawn (CPU-computed)\nuniform float uRippleAmp[4]; // shared 0..1 decay envelope per slot (CPU-computed; 0 = slot free)\nuniform float uPointerRipple; // THIS wave's ripple amplitude (scales the shared envelope)\nconst float RIPPLE_WAVE_SPEED = 0.85; // NDC/s the ring crest travels outward\nconst float RIPPLE_SIGMA = 0.14; // gaussian half-width of the travelling packet (NDC)\nconst float RIPPLE_FREQ = 11.0; // oscillation within the packet (one crest + faint troughs)\nconst float RIPPLE_MAX_R = 1.2; // reach where the crest has fully left the frame\n#endif\n#endif\n\n// expStep: a falloff from 1 (at x=0) toward 0, sharpness set by n. The\n// max() guards pow(0, n) (= Infinity → NaN) so negative n is safe — negative n\n// just concentrates the twist toward the OTHER end instead.\nfloat expStep(float x, float n){ return exp2(-exp2(n) * pow(max(x, 1.0e-3), n)); }\n\n// rotationMatrix (mat4), used row-vector style: pos = (vec4(pos,1) * R).xyz\nmat4 rotationMatrix(vec3 axis, float angle){\n axis = normalize(axis);\n float s = sin(angle), c = cos(angle), oc = 1.0 - c;\n return mat4(\n oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s, 0.0,\n oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s, 0.0,\n oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c, 0.0,\n 0.0, 0.0, 0.0, 1.0\n );\n}\n\nvoid main(){\n vUv = uv;\n#ifndef LOOP_MOTION\n float t = uTime * uSpeed + uSeed;\n#endif\n\n#ifdef LOOP_MOTION\n // Seamless loop: rather than scrolling the noise field linearly by t (which never repeats),\n // sample it on a circle of radius loopR at angle loopTheta — exactly periodic with period\n // uLoopSeconds. The tangential speed loopR·dθ/dt equals uSpeed, so the looped motion advances\n // at the same rate as the linear drift, just curved into a closed orbit (it orbits rather than\n // drifts — the trade-off for a seamless loop, hence opt-in). uSeed offsets the phase so stacked\n // waves keep their relative motion while sharing the single period.\n float loopTheta = uTime * (6.28318530718 / uLoopSeconds) + uSeed;\n float loopR = uSpeed * uLoopSeconds * 0.159154943092; // = uSpeed·uLoopSeconds / (2π)\n vec2 loopOff = loopR * vec2(cos(loopTheta), sin(loopTheta));\n#endif\n\n // The base geometry is already a baked hairpin fold. On top of it we deform the\n // vertices: a displacement lifts Y by simplex noise of the (x,z) position, then\n // three axis-rotations twist the strip.\n vec3 pos = position;\n#ifdef LOOP_MOTION\n pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX, pos.z * uDispFreqZ) + loopOff);\n#else\n pos.y += uDispAmount * simplexNoise(vec2(pos.x * uDispFreqX + t, pos.z * uDispFreqZ + t));\n#endif\n#ifdef DETAIL_OCTAVE\n // A second, finer octave riding on the broad swell — fine ripples on top of the big shape, a\n // shape vocabulary single-octave displacement can't reach. Shares the loop orbit so it stays\n // periodic when looping.\n#ifdef LOOP_MOTION\n pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq, pos.z * uDetailFreq) + loopOff);\n#else\n pos.y += uDetailAmount * simplexNoise(vec2(pos.x * uDetailFreq + t, pos.z * uDetailFreq + t));\n#endif\n#endif\n\n // The X-twist frequency feeding rotB. Two modes: by default uTwFreqX is used\n // directly; the variant (used by the Wave 4 preset) modulates it with\n // simplex noise indexed along the ribbon (uv.y) so the twist breathes over time.\n // We gate the wobble with a #define so the compiled program is unchanged when off.\n float twistXFreq = uTwFreqX;\n#ifdef TWIST_MOTION\n#ifdef LOOP_MOTION\n float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, 0.0) + loopOff);\n#else\n float twistXNoise = simplexNoise(vec2(vUv.y * 2.0, t));\n#endif\n twistXFreq = uTwFreqX - twistXNoise * 0.1;\n#endif\n\n // Three-axis twist: expStep falloff sets how\n // sharply each rotation concentrates toward an edge. rotA keys off uv.x, rotB/rotC\n // off uv.y; axes (0.5,0,0.5) and (0,0.5,0.5) are normalised inside rotationMatrix.\n mat4 rotA = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqY * expStep(uv.x, uTwPowY));\n mat4 rotB = rotationMatrix(vec3(0.0, 0.5, 0.5), twistXFreq * expStep(uv.y, uTwPowX));\n mat4 rotC = rotationMatrix(vec3(0.5, 0.0, 0.5), uTwFreqZ * expStep(uv.y, uTwPowZ));\n pos = (vec4(pos, 1.0) * rotA).xyz;\n pos = (vec4(pos, 1.0) * rotB).xyz;\n pos = (vec4(pos, 1.0) * rotC).xyz;\n\n#ifdef POINTER_FX\n // Pointer field: displace along the wave's own (post-twist) up-axis, weighted by a circular\n // screen-space falloff around the smoothed cursor. Everything here is ADDITIVE and fenced, so\n // the shared path above/below is untouched and byte-identical when POINTER_FX is off.\n vec4 preClip = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0);\n vec2 dp = (preClip.xy / max(preClip.w, 1.0e-6) - uPointer) * vec2(uPointerAspect, 1.0);\n float fall = smoothstep(uPointerRadius, 0.0, length(dp));\n vPointerFall = fall * uPointerActive;\n // Displacement axis = local +Y carried through the SAME three twist rotations as pos (row-vector\n // convention). Rotations are linear, so post-twist axis displacement equals pre-twist Y displacement.\n vec3 dispAxis = (((vec4(0.0, 1.0, 0.0, 0.0) * rotA) * rotB) * rotC).xyz;\n // Agitation: a fast churn octave near the cursor (additive — never rewrites base noise t, which\n // would force restructuring the shared path). Loop-safe under both time variants.\n#ifdef LOOP_MOTION\n float disp = uPointerAgitate * vPointerFall\n * simplexNoise(vec2(pos.x * uDispFreqX * 3.0, pos.z * uDispFreqZ * 3.0) + loopOff * 4.0);\n#else\n float disp = uPointerAgitate * vPointerFall\n * simplexNoise(vec2(pos.x * uDispFreqX * 3.0 + t * 4.0, pos.z * uDispFreqZ * 3.0));\n#endif\n // Membrane push/pull: a smooth dome (vPointerFall is the falloff) that swells toward you (+ repel)\n // or dents away (− attract) at the cursor, riding along with the sprung field.\n disp += uPointerPush * vPointerFall;\n // Drag-wake: pull the surface just BEHIND the moving cursor into a trailing trough. dp points\n // from cursor to vertex; \"behind\" is how far the vertex sits opposite the velocity (0 ahead → 1 a\n // radius behind), gated by speed so it only forms while dragging and heals when the cursor stops.\n vec2 velC = uPointerVel * vec2(uPointerAspect, 1.0);\n float wakeSpeed = length(velC);\n if (uPointerWake != 0.0 && wakeSpeed > 1.0e-4) {\n float behind = clamp(dot(-dp, velC) / (wakeSpeed * uPointerRadius), 0.0, 1.0);\n disp -= uPointerWake * vPointerFall * behind * smoothstep(0.05, 0.6, wakeSpeed);\n }\n#ifdef POINTER_RIPPLES\n for (int i = 0; i < 4; i++) {\n if (uRippleAmp[i] > 0.0) {\n float rd = length((preClip.xy / max(preClip.w, 1.0e-6) - uRippleOrigin[i]) * vec2(uPointerAspect, 1.0));\n // A wave PACKET whose crest travels outward at RIPPLE_WAVE_SPEED: a gaussian window centred on\n // the moving front carrying a short oscillation (a raised ring with faint trailing troughs),\n // so the energy radiates instead of throbbing at the click point. The shared uRippleAmp\n // envelope fades the whole packet over its lifetime; reach fades it as the crest leaves frame.\n float front = uRippleAge[i] * RIPPLE_WAVE_SPEED;\n float band = rd - front;\n float packet = exp(-band * band / (2.0 * RIPPLE_SIGMA * RIPPLE_SIGMA)) * cos(band * RIPPLE_FREQ);\n float reach = 1.0 - smoothstep(RIPPLE_MAX_R * 0.7, RIPPLE_MAX_R, front);\n disp += uPointerRipple * uRippleAmp[i] * packet * reach;\n }\n }\n#endif\n pos += dispAxis * disp;\n#endif\n\n // The scale / rotation / position transform lives on the mesh (modelMatrix), so the\n // orientation matches THREE's Euler-XYZ rather than an in-shader rotation order.\n vec4 world = modelMatrix * vec4(pos, 1.0);\n vWorldPos = world.xyz;\n vViewDir = cameraPosition - world.xyz;\n gl_Position = projectionMatrix * viewMatrix * world;\n vClipPosition = gl_Position;\n}\n`;\n\nexport const fragmentShader = /* glsl */ `\n#define MAX_COLORS ${MAX_COLORS}\n#define MAX_MESH_POINTS ${MAX_MESH_POINTS}\n#define MAX_LIGHTS ${MAX_LIGHTS}\n#define MAX_NOISE_BANDS ${MAX_NOISE_BANDS}\n#define PI 3.14159265359\n\n${simplex2d}\n\n${colorUniforms}\nuniform float uDebug; // dev: 1 = show crease, 2 = show derivative normal\nuniform float uSheen; // white-lift on the flat (low-crease) areas (1 = full)\nuniform float uRoundness; // pose-robust normal-based roundness/thickness strength\nuniform float uIridescence; // thin-film hue shift with view angle (0 = off)\nuniform float uFiberCount;\nuniform float uFiberStrength;\nuniform float uTexture;\nuniform float uCreaseLight;\nuniform float uCreaseSharpness;\nuniform float uCreaseSoftness;\nuniform float uEdgeFade;\nuniform vec2 uResolution;\nuniform float uAmbient;\nuniform int uNumLights;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightIntensity[MAX_LIGHTS];\nuniform int uNumNoiseBands;\nuniform vec4 uNoiseBandBounds[MAX_NOISE_BANDS]; // (startX, endX, startY, endY)\nuniform vec4 uNoiseBandParams[MAX_NOISE_BANDS]; // (feather, strength, frequency, colorAttenuation)\nuniform float uNoiseBandParaPow[MAX_NOISE_BANDS];\n\nvarying vec2 vUv;\nvarying vec3 vWorldPos;\nvarying vec3 vViewDir;\n#ifdef DEPTH_TINT\nuniform float uDepthTint;\nuniform vec3 uDepthTintColor;\nvarying vec4 vClipPosition; // clip-space depth (written by the vertex shader for both programs)\n#endif\n#ifdef EDGE_FEATHER\nuniform float uEdgeFeather; // ribbon long-edge softness (only when it differs from the 0.1 default)\n#endif\n#ifdef POINTER_FX\nuniform float uPointerThin; // 0..1 local translucency near the cursor\nuniform float uPointerHue; // degrees, local hue rotation near the cursor\nuniform float uPointerLighten; // -1..1 local brightness lift near the cursor\nvarying float vPointerFall; // falloff × presence, written by the vertex shader\n#endif\n\n// Cheap value hash for the optional grain overlay (distinct from the simplex hash).\nfloat grainHash(vec2 p){ return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }\n\nfloat parabola(float x, float k){ return pow(4.0 * x * (1.0 - x), k); }\nfloat mapLinear(float v, float a, float b, float c, float d){ return c + (v - a) * (d - c) / (b - a); }\n\n${colorFns}\n\n// Striations: a subtle high-frequency simplex-noise grain ADDED to the\n// colour — colour-matched (weaker where blue is high), only near folds (crease), and\n// concentrated toward the ends (parabola). Blends in rather than reading as hard lines.\nvec3 surfaceStreaks(vec2 uv, vec3 color, float crease){\n float strength = uFiberStrength; // default 0.2\n float freq = uFiberCount; // default 600\n float colorAtten = 0.9;\n float paraPow = 3.0;\n // Noise bands: inside each rectangular uv region the\n // fiber params are overridden, so the streaks vary per region instead of uniform.\n for (int i = 0; i < MAX_NOISE_BANDS; i++) {\n if (i >= uNumNoiseBands) break;\n vec4 b = uNoiseBandBounds[i];\n vec4 prm = uNoiseBandParams[i];\n float feather = max(prm.x, 1.0e-4);\n float blend =\n smoothstep(b.x - feather, b.x, uv.x) * (1.0 - smoothstep(b.y, b.y + feather, uv.x)) *\n smoothstep(b.z - feather, b.z, uv.y) * (1.0 - smoothstep(b.w, b.w + feather, uv.y));\n strength = mix(strength, prm.y, blend);\n freq = mix(freq, prm.z, blend);\n colorAtten = mix(colorAtten, prm.w, blend);\n paraPow = mix(paraPow, uNoiseBandParaPow[i], blend);\n }\n // The high frequency runs along uv.x (the ribbon's length) so the streaks read as\n // fine lengthwise fibers; end-weighted by 1 - parabola(uv.x).\n float p = 1.0 - parabola(uv.x, paraPow);\n float n0 = simplexNoise(vec2(uv.x * 0.1, uv.y * 0.5));\n float n1 = simplexNoise(vec2(uv.x * (freq + freq * 0.5 * n0), uv.y * 4.0 * n0));\n n1 = mapLinear(n1, -1.0, 1.0, 0.0, 1.0);\n color += n1 * strength * (1.0 - color.b * colorAtten) * crease * p;\n return color;\n}\n\nvoid main(){\n // crease: a foreshortening / fold detector from the screen-space uv derivative.\n // It drives BOTH the roundness shading and where the streaks appear — this is what\n // gives the wave its thickness without any normal-based lighting.\n float crease = dFdy(vUv).y * uResolution.y * uCreaseLight;\n crease = clamp(mapLinear(crease, -1.0, 1.0, 0.0, 1.0), 0.0, 1.0);\n crease = pow(crease, uCreaseSharpness);\n crease = clamp(smoothstep(0.0, uCreaseSoftness, crease), 0.0, 1.0);\n\n // Debug visualisations (dev): 1 = crease value, 2 = derivative surface normal.\n if (uDebug > 0.5) {\n if (uDebug < 1.5) { gl_FragColor = vec4(vec3(crease), 1.0); return; }\n vec3 dn = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n gl_FragColor = vec4(dn * 0.5 + 0.5, 1.0); return;\n }\n\n // Colour: sample the baked 2D palette texture, or fall back to the procedural 1-D\n // gradient (see waveBaseColor).\n vec3 col = waveBaseColor(vUv);\n col = surfaceStreaks(vUv, col, crease);\n col = applyColorGrade(col);\n\n#ifdef POINTER_FX\n // Local hue rotation + brightness lift near the cursor (both fade out with vPointerFall).\n col = hueShift(col, radians(uPointerHue) * vPointerFall);\n col *= 1.0 + uPointerLighten * vPointerFall;\n#endif\n\n // Iridescence: a thin-film / holographic hue that shifts with view angle. Reuses the same\n // camera-facing ratio as roundness (recomputed here, since roundness may be off): grazing parts\n // of the ribbon (low facing) shift hue most, so the colour flows as the ribbon curves. Skipped\n // at 0, so the compiled result is unchanged when off.\n if (uIridescence > 0.001) {\n vec3 iridN = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n float iridFacing = abs(dot(iridN, normalize(vViewDir)));\n col = hueShift(col, (1.0 - iridFacing) * uIridescence * PI);\n }\n\n // Sheen: lift the flat (low-crease) areas toward white. This is\n // pose-dependent (it keys off dFdy(uv.y)), so we keep it gentle and add a robust term.\n col += (1.0 - crease) * 0.25 * uSheen;\n\n // Pose-robust roundness: shade by the camera-facing ratio of the derivative surface\n // normal so the ribbon reads as a rounded, grabbable solid from any angle. Grazing\n // edges darken into shadow (defining the rounded form), the body keeps its full colour,\n // and the most face-on sliver catches a soft highlight. uRoundness = strength.\n if (uRoundness > 0.001) {\n vec3 volN = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n float facing = abs(dot(volN, normalize(vViewDir))); // 1 = facing camera, 0 = edge-on\n col *= mix(1.0 - 0.6 * uRoundness, 1.0, facing); // deepen grazing edges → solid form\n col += smoothstep(0.65, 1.0, facing) * uRoundness * 0.18; // soft highlight on the facing body\n }\n\n // Optional positionable lights (our feature) — additive & gentle, on top of the\n // base shading so the default look is preserved. A finely-subdivided mesh\n // keeps this derivative normal smooth.\n if (uNumLights > 0) {\n vec3 N = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));\n vec3 Vd = normalize(vViewDir);\n if (dot(N, Vd) < 0.0) N = -N;\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uNumLights) break;\n vec3 L = normalize(uLightPos[i] - vWorldPos);\n vec3 lc = uLightColor[i] * uLightIntensity[i];\n float diff = max(dot(N, L), 0.0);\n float spec = pow(max(dot(N, normalize(L + Vd)), 0.0), 28.0);\n col += col * diff * lc * 0.16 + spec * lc * 0.10;\n }\n }\n col *= 0.55 + clamp(uAmbient, 0.0, 1.0); // overall level; default 0.45 => x1.0 (neutral)\n\n#ifdef DEPTH_TINT\n // Depth tint: fade far fragments toward a colour so a multi-wave stack gains atmospheric\n // separation — near strands keep their colour, far ones recede. Reuses the clip-space depth the\n // wireframe theme fades with (clamp(z*6), where 1 = far).\n col = mix(col, uDepthTintColor, clamp(vClipPosition.z * 6.0, 0.0, 1.0) * uDepthTint);\n#endif\n\n if (uTexture > 0.001) col *= 1.0 + (grainHash(vUv * 850.0) - 0.5) * uTexture * 0.25;\n\n // Soft long edges + optional viewport-edge fade. The edge softness is the hardcoded 0.1 by\n // default (literal branch → byte-identical); EDGE_FEATHER swaps in the uEdgeFeather knob only\n // when it differs, so razor-crisp or vapor-soft edges are both reachable.\n#ifdef EDGE_FEATHER\n float ribEdge =\n smoothstep(0.0, uEdgeFeather, vUv.y) * (1.0 - smoothstep(1.0 - uEdgeFeather, 1.0, vUv.y));\n#else\n float ribEdge = smoothstep(0.0, 0.1, vUv.y) * (1.0 - smoothstep(0.9, 1.0, vUv.y));\n#endif\n float alpha = uOpacity * ribEdge;\n#ifdef POINTER_FX\n alpha *= clamp(1.0 - uPointerThin * vPointerFall, 0.0, 1.0); // solid: local translucency\n#endif\n if (uEdgeFade > 0.001) {\n vec2 sc = gl_FragCoord.xy / max(uResolution, vec2(1.0));\n float vig =\n smoothstep(0.0, uEdgeFade, sc.x) * (1.0 - smoothstep(1.0 - uEdgeFade, 1.0, sc.x)) *\n smoothstep(0.0, uEdgeFade, sc.y) * (1.0 - smoothstep(1.0 - uEdgeFade, 1.0, sc.y));\n alpha *= vig;\n }\n\n // Deep \"squared\" hero colour: formerly done by a framebuffer-squaring blend that REPLACED the\n // destination (punching holes at soft edges / where waves overlap). Squaring here + normal\n // premultiplied compositing (see applyBlendMode) keeps the deep colour and blends correctly.\n col = clamp(col, 0.0, 1.0);\n // Square colour AND alpha so the soft ribbon edges keep the crisp, thin feather of the original\n // squared-blend look — but now composited (premultiplied) rather than replace-blended, so they\n // no longer punch holes. Over an opaque background alpha² still resolves to fully opaque.\n if (uSquared > 0.5) { col *= col; alpha *= alpha; }\n gl_FragColor = vec4(col, alpha);\n#ifdef PREMULTIPLIED_ALPHA\n gl_FragColor.rgb *= gl_FragColor.a;\n#endif\n}\n`;\n\n// ---- Wireframe \"thin-line\" theme ----\n// The same wave geometry, but instead of a solid surface the colour is carved into fine\n// vertical lines (abs(sin(uv.x * lineAmount))) whose thickness scales with the screen-\n// space uv derivative, then mixed line<->background with a depth fade. Used by the dark\n// hero preset. hueShift takes degrees (radians() here) to match the light shader.\nexport const lineFragmentShader = /* glsl */ `\n#define MAX_COLORS ${MAX_COLORS}\n#define MAX_MESH_POINTS ${MAX_MESH_POINTS}\n#define PI 3.14159265359\n\n${simplex2d}\n\n${colorUniforms}\nuniform float uLineAmount; // default 425\nuniform float uLineThickness; // default 1\nuniform float uLineDerivativePower; // default 0.95\nuniform float uMaxWidth; // default 1232\nuniform vec3 uClearColor; // = page background colour (shown between the lines)\n\nvarying vec2 vUv;\nvarying vec4 vClipPosition;\n#ifdef POINTER_FX\nuniform float uPointerThin; // 0..1 — strands taper to hairlines near the cursor\nuniform float uPointerHue; // degrees, local hue rotation near the cursor\nuniform float uPointerLighten; // -1..1 local brightness lift near the cursor\nvarying float vPointerFall; // falloff × presence, written by the vertex shader\n#endif\n\n${colorFns}\n\nvoid main(){\n // Same 2D palette sample + colour ops as the solid theme.\n vec3 color = applyColorGrade(waveBaseColor(vUv));\n\n#ifdef POINTER_FX\n color = hueShift(color, radians(uPointerHue) * vPointerFall);\n color *= 1.0 + uPointerLighten * vPointerFall;\n#endif\n\n // Carve into fine vertical lines; thickness from the screen-space uv derivative.\n vec2 dy = dFdy(vUv);\n float lineThickness = uLineThickness * pow(abs(dy.x * uMaxWidth), uLineDerivativePower);\n#ifdef POINTER_FX\n lineThickness *= clamp(1.0 - uPointerThin * vPointerFall, 0.0, 1.0); // wireframe: taper strands\n#endif\n float a = abs(sin(vUv.x * uLineAmount));\n a = smoothstep(lineThickness, 0.0, a);\n\n // Depth fade: the wave recedes into the background colour with depth. Watch the\n // argument order: clamp(0.0, 1.0, z*6) is a swapped-args trap — it clamps the\n // constant 0.0 into [1.0, z*6], i.e. min(1.0, z*6), which (with our ortho clip.z\n // range) collapses the whole wave to the background. The correct clamp(z*6, 0, 1)\n // gives the proper subtle far-end fade and thin-line look.\n float depthFade = clamp(vClipPosition.z * 6.0, 0.0, 1.0);\n color = mix(uClearColor, color, a * (1.0 - depthFade));\n if (uSquared > 0.5) color *= color; // deep \"squared\" look, now composited not replace-blended\n gl_FragColor = vec4(color, uOpacity);\n#ifdef PREMULTIPLIED_ALPHA\n gl_FragColor.rgb *= gl_FragColor.a;\n#endif\n}\n`;\n\n// ---- Post pass: viewport-edge soft-focus blur + dither grain ----\n\nexport const postVertexShader = /* glsl */ `\nvarying vec2 vUv;\nvoid main(){\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`;\n\nexport const postFragmentShader = /* glsl */ `\nuniform sampler2D tDiffuse;\nuniform vec2 uResolution;\nuniform float uBlurAmount;\nuniform int uBlurSamples;\nuniform float uGrainAmount;\nuniform float uTime;\nvarying vec2 vUv;\n\nfloat random2(vec2 st){ return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453); }\n\n// Angular (spin) blur: rotate the sample coord around the centre and\n// accumulate — a tangential smear that grows toward the edges. Carries alpha so a\n// transparent background survives the post pass.\nvec4 blurAngular(sampler2D tex, vec2 uv, float angle, int samples){\n vec4 total = vec4(0.0);\n vec2 coord = uv - 0.5;\n float dist = 1.0 / float(samples);\n vec2 dir = vec2(cos(angle * dist), sin(angle * dist));\n mat2 rot = mat2(dir.x, dir.y, -dir.y, dir.x);\n for (int i = 0; i < 64; i++){\n if (i >= samples) break;\n total += texture2D(tex, coord + 0.5);\n coord = coord * rot; // row-vector order (coord * rot) sets the spin direction\n }\n return total * dist;\n}\n\nvoid main(){\n vec4 sceneColor = texture2D(tDiffuse, vUv);\n vec4 blurColor = blurAngular(tDiffuse, vUv, uBlurAmount, uBlurSamples);\n // blurPower: keep a sharp band weighted to the middle, blurring toward top & bottom.\n float blurPower = smoothstep(0.0, 0.7, vUv.y) - smoothstep(0.2, 1.0, vUv.y);\n vec4 color = mix(blurColor, sceneColor, blurPower);\n // Static film grain: keyed off gl_FragCoord only (no uTime), so it doesn't flicker.\n color.rgb += mix(uGrainAmount, -uGrainAmount, random2(gl_FragCoord.xy * 0.01)) * (4.0 / 255.0);\n gl_FragColor = color; // preserve alpha → transparent background works\n}\n`;\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,MAAM,YAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoC7B,MAAM,gBAA2B;;;;;;;;;;;;;;;;;;;;;;;;AA2BjC,MAAM,WAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyF5B,MAAa,eAA0B;EACrC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkLZ,MAAa,iBAA4B;;;;;;;EAOvC,UAAU;;EAEV,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+Cd,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4JX,MAAa,qBAAgC;;;;;EAK3C,UAAU;;EAEV,cAAc;;;;;;;;;;;;;;;;EAgBd,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCX,MAAa,mBAA8B;;;;;;;AAQ3C,MAAa,qBAAgC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StudioConfig } from "../config/model.js";
|
|
2
2
|
import { WaveRenderer } from "../renderer/WaveRenderer.js";
|
|
3
3
|
import { core_loader_d_exports } from "../core-loader.js";
|
|
4
|
+
import { PosterFit } from "./poster.js";
|
|
4
5
|
|
|
5
6
|
//#region src/shell/createWave.d.ts
|
|
6
7
|
/** Why the shell showed the poster instead of a live wave. */
|
|
@@ -12,6 +13,10 @@ type CoreModule = typeof core_loader_d_exports;
|
|
|
12
13
|
interface WaveOptions {
|
|
13
14
|
/** Poster URL / data-URI. Defaults to adopting the container's `<img data-wave3d-poster>` (SSR). */
|
|
14
15
|
poster?: string;
|
|
16
|
+
/** Poster `object-fit`. Default `"fill"` — matches the canvas (which renders edge-to-edge at the
|
|
17
|
+
* container's aspect), so a poster captured at that aspect hands off with no visible jump. Use
|
|
18
|
+
* `"cover"` to crop a different-aspect placeholder instead of stretching it. See {@link PosterFit}. */
|
|
19
|
+
posterFit?: PosterFit;
|
|
15
20
|
/** Wait until the container nears the viewport before fetching the engine. Default true. */
|
|
16
21
|
lazy?: boolean;
|
|
17
22
|
/** IntersectionObserver margin for the lazy trigger. Default "200px". */
|
|
@@ -54,6 +59,9 @@ interface WaveHandle {
|
|
|
54
59
|
snapshot(options?: SnapshotOptions): Promise<Blob | null>;
|
|
55
60
|
/** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */
|
|
56
61
|
set(config: Partial<StudioConfig>): void;
|
|
62
|
+
/** Feed a `custom:<name>` interaction input for `custom:*` bindings. Staged (last value per name)
|
|
63
|
+
* before upgrade and replayed once the renderer is live; a no-op if no binding consumes it. */
|
|
64
|
+
setInteractionInput(name: string, value: number): void;
|
|
57
65
|
play(): void;
|
|
58
66
|
pause(): void;
|
|
59
67
|
/** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */
|
|
@@ -69,5 +77,5 @@ declare function createWave(container: HTMLElement, config?: Partial<StudioConfi
|
|
|
69
77
|
/** The drop-in embed contract: an alias of {@link createWave}. */
|
|
70
78
|
declare const mountWave: typeof createWave;
|
|
71
79
|
//#endregion
|
|
72
|
-
export { FallbackReason, SnapshotOptions, WaveHandle, WaveOptions, WaveState, createWave, mountWave };
|
|
80
|
+
export { FallbackReason, type PosterFit, SnapshotOptions, WaveHandle, WaveOptions, WaveState, createWave, mountWave };
|
|
73
81
|
//# sourceMappingURL=createWave.d.ts.map
|
package/dist/shell/createWave.js
CHANGED
|
@@ -12,12 +12,13 @@ function createWaveImpl(loadCore, container, config, options) {
|
|
|
12
12
|
let renderer = null;
|
|
13
13
|
let staged = { ...config };
|
|
14
14
|
if (options.paused !== void 0) staged.paused = options.paused;
|
|
15
|
+
const stagedInputs = /* @__PURE__ */ new Map();
|
|
15
16
|
let aborted = false;
|
|
16
17
|
let io = null;
|
|
17
18
|
let lostTimer;
|
|
18
19
|
let lossCount = 0;
|
|
19
20
|
ensurePositioned(container);
|
|
20
|
-
const poster = setupPoster(container, options.poster);
|
|
21
|
+
const poster = setupPoster(container, options.poster, options.posterFit);
|
|
21
22
|
function setState(next) {
|
|
22
23
|
if (state === next) return;
|
|
23
24
|
state = next;
|
|
@@ -72,6 +73,7 @@ function createWaveImpl(loadCore, container, config, options) {
|
|
|
72
73
|
canvas.addEventListener("webglcontextlost", onContextLost, false);
|
|
73
74
|
canvas.addEventListener("webglcontextrestored", onContextRestored, false);
|
|
74
75
|
renderer.start();
|
|
76
|
+
for (const [name, value] of stagedInputs) renderer.setInteractionInput(name, value);
|
|
75
77
|
setState("running");
|
|
76
78
|
options.onReady?.(renderer);
|
|
77
79
|
if (poster) requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
@@ -125,6 +127,10 @@ function createWaveImpl(loadCore, container, config, options) {
|
|
|
125
127
|
...next
|
|
126
128
|
};
|
|
127
129
|
},
|
|
130
|
+
setInteractionInput(name, value) {
|
|
131
|
+
if (renderer) renderer.setInteractionInput(name, value);
|
|
132
|
+
else stagedInputs.set(name, value);
|
|
133
|
+
},
|
|
128
134
|
play() {
|
|
129
135
|
if (renderer) {
|
|
130
136
|
renderer.getConfig().paused = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createWave.js","names":[],"sources":["../../src/shell/createWave.ts"],"sourcesContent":["import type { StudioConfig } from \"../config/model\";\nimport type { WaveRenderer, WaveRendererOptions } from \"../renderer/WaveRenderer\";\nimport { hasWebGL, prefersReducedMotion, prefersReducedData } from \"./probe\";\nimport { setupPoster, ensurePositioned, type Poster } from \"./poster\";\n\n/** Why the shell showed the poster instead of a live wave. */\nexport type FallbackReason =\n | \"no-webgl\"\n | \"reduced-motion\"\n | \"save-data\"\n | \"context-lost\"\n | \"load-error\";\n\n/** poster → loading → running, or → fallback (permanent poster). */\nexport type WaveState = \"poster\" | \"loading\" | \"running\" | \"fallback\";\n\n/** The heavy module fetched on upgrade. */\ntype CoreModule = typeof import(\"../core-loader\");\n\nexport interface WaveOptions {\n /** Poster URL / data-URI. Defaults to adopting the container's `<img data-wave3d-poster>` (SSR). */\n poster?: string;\n /** Wait until the container nears the viewport before fetching the engine. Default true. */\n lazy?: boolean;\n /** IntersectionObserver margin for the lazy trigger. Default \"200px\". */\n rootMargin?: string;\n /** \"auto\" probes WebGL (with failIfMajorPerformanceCaveat); \"force\" skips the probe; \"off\" stays a poster. */\n webgl?: \"auto\" | \"force\" | \"off\";\n /** Forward prefers-reduced-motion to the renderer (freezes to a full static frame). Default true. */\n respectReducedMotion?: boolean;\n /** With reduced motion: \"static\" upgrades to a frozen frame; \"poster\" stays a poster. Default \"static\". */\n reducedMotionBehavior?: \"static\" | \"poster\";\n /** Keep a permanent poster when the user has Save-Data on. Default true. */\n respectSaveData?: boolean;\n /** Poster→canvas crossfade duration (ms). Default 300. */\n fadeMs?: number;\n /** Start paused. */\n paused?: boolean;\n onReady?(renderer: WaveRenderer): void;\n onFallback?(reason: FallbackReason): void;\n onStateChange?(state: WaveState): void;\n /** Seam for the standalone/CDN build to supply the core synchronously (three already bundled). */\n loadCore?(): Promise<CoreModule>;\n}\n\nexport interface SnapshotOptions {\n /** Image MIME type. Default `\"image/webp\"`. */\n type?: string;\n /** Encoder quality 0–1 for lossy types. */\n quality?: number;\n /** Render with a transparent background. Default true. */\n transparent?: boolean;\n /** Render a fixed animation-time for a reproducible frame (default: the live frame). Poster\n * captures should pass `0` — the frame the wave opens on, so the file doesn't churn per capture. */\n time?: number;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Capture the current live frame as an image Blob (a poster you can host/cache). Resolves `null`\n * until the wave is running (poster / fallback / pre-upgrade) — wait for {@link WaveOptions.onReady}\n * (or the element's `wave3d-ready` event) first. */\n snapshot(options?: SnapshotOptions): Promise<Blob | null>;\n /** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */\n set(config: Partial<StudioConfig>): void;\n play(): void;\n pause(): void;\n /** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */\n destroy(): void;\n}\n\n/**\n * The shell implementation. `loadCore` is an explicit parameter (not read from options) so the\n * standalone/CDN build can pass a synchronous core and NOT bundle the dynamic-import path — its\n * output stays a single file. The public {@link createWave} supplies the dynamic-import default.\n */\nexport function createWaveImpl(\n loadCore: () => Promise<CoreModule>,\n container: HTMLElement,\n config: Partial<StudioConfig>,\n options: WaveOptions,\n): WaveHandle {\n const {\n lazy = true,\n rootMargin = \"200px\",\n webgl = \"auto\",\n respectReducedMotion = true,\n reducedMotionBehavior = \"static\",\n respectSaveData = true,\n fadeMs = 300,\n } = options;\n\n let state: WaveState = \"poster\";\n let renderer: WaveRenderer | null = null;\n let staged: Partial<StudioConfig> = { ...config };\n if (options.paused !== undefined) staged.paused = options.paused;\n\n let aborted = false;\n let io: IntersectionObserver | null = null;\n let lostTimer: ReturnType<typeof setTimeout> | undefined;\n let lossCount = 0;\n\n ensurePositioned(container);\n const poster: Poster | null = setupPoster(container, options.poster);\n\n function setState(next: WaveState): void {\n if (state === next) return;\n state = next;\n options.onStateChange?.(next);\n }\n\n function fallback(reason: FallbackReason): void {\n setState(\"fallback\");\n poster?.show();\n options.onFallback?.(reason);\n }\n\n function onContextRestored(): void {\n clearTimeout(lostTimer); // three rebuilt the context in time; stay live\n }\n\n function onContextLost(): void {\n lossCount += 1;\n clearTimeout(lostTimer);\n if (lossCount >= 2) {\n teardownRenderer();\n fallback(\"context-lost\");\n return;\n }\n // three (WaveRenderer) tries to restore; if it hasn't within ~4s, give up to the poster.\n lostTimer = setTimeout(() => {\n teardownRenderer();\n fallback(\"context-lost\");\n }, 4000);\n }\n\n function teardownRenderer(): void {\n if (!renderer) return;\n const canvas = renderer.renderer.domElement;\n canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n canvas.removeEventListener(\"webglcontextrestored\", onContextRestored);\n renderer.dispose();\n renderer = null;\n }\n\n async function upgrade(): Promise<void> {\n setState(\"loading\");\n let core: CoreModule;\n try {\n core = await loadCore();\n } catch {\n if (!aborted) fallback(\"load-error\");\n return;\n }\n if (aborted) return;\n\n const full: StudioConfig = { ...core.createDefaultConfig(), ...staged };\n const rendererOptions: WaveRendererOptions = { respectReducedMotion };\n renderer = new core.WaveRenderer(container, full, rendererOptions);\n const canvas = renderer.renderer.domElement;\n canvas.addEventListener(\"webglcontextlost\", onContextLost, false);\n canvas.addEventListener(\"webglcontextrestored\", onContextRestored, false);\n renderer.start();\n setState(\"running\");\n options.onReady?.(renderer);\n\n if (poster) {\n // Crossfade only after two frames, so the wave has definitely painted first.\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (!aborted && renderer) poster.fadeOut(fadeMs);\n }),\n );\n }\n }\n\n function probeAndUpgrade(): void {\n if (aborted) return;\n if (webgl === \"auto\" && !hasWebGL()) {\n fallback(\"no-webgl\");\n return;\n }\n void upgrade();\n }\n\n function begin(): void {\n // Permanent-poster gates (checked before any lazy wait or engine fetch).\n if (webgl === \"off\") return; // deliberate poster-only mode — stay \"poster\", no fallback callback\n if (respectSaveData && prefersReducedData()) return fallback(\"save-data\");\n if (respectReducedMotion && reducedMotionBehavior === \"poster\" && prefersReducedMotion()) {\n return fallback(\"reduced-motion\");\n }\n if (lazy && typeof IntersectionObserver !== \"undefined\") {\n io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n io?.disconnect();\n io = null;\n probeAndUpgrade();\n }\n },\n { rootMargin },\n );\n io.observe(container);\n } else {\n probeAndUpgrade();\n }\n }\n\n const handle: WaveHandle = {\n get state() {\n return state;\n },\n get renderer() {\n return renderer;\n },\n snapshot(opts = {}) {\n if (!renderer) return Promise.resolve(null);\n const { type = \"image/webp\", quality, transparent = true, time } = opts;\n return renderer.captureImage(type, transparent, quality, time);\n },\n set(next) {\n if (renderer) {\n renderer.setConfig({ ...renderer.getConfig(), ...next });\n renderer.refreshPlayback(); // setConfig doesn't re-evaluate `paused` on its own\n } else {\n staged = { ...staged, ...next };\n }\n },\n play() {\n if (renderer) {\n renderer.getConfig().paused = false;\n renderer.refreshPlayback();\n } else {\n staged.paused = false;\n }\n },\n pause() {\n if (renderer) {\n renderer.getConfig().paused = true;\n renderer.refreshPlayback();\n } else {\n staged.paused = true;\n }\n },\n destroy() {\n aborted = true;\n io?.disconnect();\n io = null;\n clearTimeout(lostTimer);\n teardownRenderer();\n poster?.remove();\n },\n };\n\n begin();\n return handle;\n}\n\n/**\n * Mount a self-optimizing wave into a container: shows a poster immediately, then — lazily, and only\n * when the browser can actually run it — fetches the engine, builds the renderer, and crossfades in.\n * Falls back to the poster on no-WebGL / save-data / reduced-motion / context-loss / load errors.\n * No static three import: the engine arrives via a dynamic import, so the shell stays tiny.\n */\nexport function createWave(\n container: HTMLElement,\n config: Partial<StudioConfig> = {},\n options: WaveOptions = {},\n): WaveHandle {\n return createWaveImpl(\n options.loadCore ?? (() => import(\"../core-loader\")),\n container,\n config,\n options,\n );\n}\n\n/** The drop-in embed contract: an alias of {@link createWave}. */\nexport const mountWave = createWave;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createWave.js","names":[],"sources":["../../src/shell/createWave.ts"],"sourcesContent":["import type { StudioConfig } from \"../config/model\";\nimport type { WaveRenderer, WaveRendererOptions } from \"../renderer/WaveRenderer\";\nimport { hasWebGL, prefersReducedMotion, prefersReducedData } from \"./probe\";\nimport { setupPoster, ensurePositioned, type Poster, type PosterFit } from \"./poster\";\n\nexport type { PosterFit } from \"./poster\";\n\n/** Why the shell showed the poster instead of a live wave. */\nexport type FallbackReason =\n | \"no-webgl\"\n | \"reduced-motion\"\n | \"save-data\"\n | \"context-lost\"\n | \"load-error\";\n\n/** poster → loading → running, or → fallback (permanent poster). */\nexport type WaveState = \"poster\" | \"loading\" | \"running\" | \"fallback\";\n\n/** The heavy module fetched on upgrade. */\ntype CoreModule = typeof import(\"../core-loader\");\n\nexport interface WaveOptions {\n /** Poster URL / data-URI. Defaults to adopting the container's `<img data-wave3d-poster>` (SSR). */\n poster?: string;\n /** Poster `object-fit`. Default `\"fill\"` — matches the canvas (which renders edge-to-edge at the\n * container's aspect), so a poster captured at that aspect hands off with no visible jump. Use\n * `\"cover\"` to crop a different-aspect placeholder instead of stretching it. See {@link PosterFit}. */\n posterFit?: PosterFit;\n /** Wait until the container nears the viewport before fetching the engine. Default true. */\n lazy?: boolean;\n /** IntersectionObserver margin for the lazy trigger. Default \"200px\". */\n rootMargin?: string;\n /** \"auto\" probes WebGL (with failIfMajorPerformanceCaveat); \"force\" skips the probe; \"off\" stays a poster. */\n webgl?: \"auto\" | \"force\" | \"off\";\n /** Forward prefers-reduced-motion to the renderer (freezes to a full static frame). Default true. */\n respectReducedMotion?: boolean;\n /** With reduced motion: \"static\" upgrades to a frozen frame; \"poster\" stays a poster. Default \"static\". */\n reducedMotionBehavior?: \"static\" | \"poster\";\n /** Keep a permanent poster when the user has Save-Data on. Default true. */\n respectSaveData?: boolean;\n /** Poster→canvas crossfade duration (ms). Default 300. */\n fadeMs?: number;\n /** Start paused. */\n paused?: boolean;\n onReady?(renderer: WaveRenderer): void;\n onFallback?(reason: FallbackReason): void;\n onStateChange?(state: WaveState): void;\n /** Seam for the standalone/CDN build to supply the core synchronously (three already bundled). */\n loadCore?(): Promise<CoreModule>;\n}\n\nexport interface SnapshotOptions {\n /** Image MIME type. Default `\"image/webp\"`. */\n type?: string;\n /** Encoder quality 0–1 for lossy types. */\n quality?: number;\n /** Render with a transparent background. Default true. */\n transparent?: boolean;\n /** Render a fixed animation-time for a reproducible frame (default: the live frame). Poster\n * captures should pass `0` — the frame the wave opens on, so the file doesn't churn per capture. */\n time?: number;\n}\n\nexport interface WaveHandle {\n readonly state: WaveState;\n readonly renderer: WaveRenderer | null;\n /** Capture the current live frame as an image Blob (a poster you can host/cache). Resolves `null`\n * until the wave is running (poster / fallback / pre-upgrade) — wait for {@link WaveOptions.onReady}\n * (or the element's `wave3d-ready` event) first. */\n snapshot(options?: SnapshotOptions): Promise<Blob | null>;\n /** Merge a partial config. Staged before upgrade; after, setConfig() then refreshPlayback(). */\n set(config: Partial<StudioConfig>): void;\n /** Feed a `custom:<name>` interaction input for `custom:*` bindings. Staged (last value per name)\n * before upgrade and replayed once the renderer is live; a no-op if no binding consumes it. */\n setInteractionInput(name: string, value: number): void;\n play(): void;\n pause(): void;\n /** Safe to call in any state (aborts a pending upgrade, disposes a live renderer, removes the poster). */\n destroy(): void;\n}\n\n/**\n * The shell implementation. `loadCore` is an explicit parameter (not read from options) so the\n * standalone/CDN build can pass a synchronous core and NOT bundle the dynamic-import path — its\n * output stays a single file. The public {@link createWave} supplies the dynamic-import default.\n */\nexport function createWaveImpl(\n loadCore: () => Promise<CoreModule>,\n container: HTMLElement,\n config: Partial<StudioConfig>,\n options: WaveOptions,\n): WaveHandle {\n const {\n lazy = true,\n rootMargin = \"200px\",\n webgl = \"auto\",\n respectReducedMotion = true,\n reducedMotionBehavior = \"static\",\n respectSaveData = true,\n fadeMs = 300,\n } = options;\n\n let state: WaveState = \"poster\";\n let renderer: WaveRenderer | null = null;\n let staged: Partial<StudioConfig> = { ...config };\n if (options.paused !== undefined) staged.paused = options.paused;\n // Interaction inputs fed before the renderer exists — last value per name, replayed on upgrade.\n const stagedInputs = new Map<string, number>();\n\n let aborted = false;\n let io: IntersectionObserver | null = null;\n let lostTimer: ReturnType<typeof setTimeout> | undefined;\n let lossCount = 0;\n\n ensurePositioned(container);\n const poster: Poster | null = setupPoster(container, options.poster, options.posterFit);\n\n function setState(next: WaveState): void {\n if (state === next) return;\n state = next;\n options.onStateChange?.(next);\n }\n\n function fallback(reason: FallbackReason): void {\n setState(\"fallback\");\n poster?.show();\n options.onFallback?.(reason);\n }\n\n function onContextRestored(): void {\n clearTimeout(lostTimer); // three rebuilt the context in time; stay live\n }\n\n function onContextLost(): void {\n lossCount += 1;\n clearTimeout(lostTimer);\n if (lossCount >= 2) {\n teardownRenderer();\n fallback(\"context-lost\");\n return;\n }\n // three (WaveRenderer) tries to restore; if it hasn't within ~4s, give up to the poster.\n lostTimer = setTimeout(() => {\n teardownRenderer();\n fallback(\"context-lost\");\n }, 4000);\n }\n\n function teardownRenderer(): void {\n if (!renderer) return;\n const canvas = renderer.renderer.domElement;\n canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n canvas.removeEventListener(\"webglcontextrestored\", onContextRestored);\n renderer.dispose();\n renderer = null;\n }\n\n async function upgrade(): Promise<void> {\n setState(\"loading\");\n let core: CoreModule;\n try {\n core = await loadCore();\n } catch {\n if (!aborted) fallback(\"load-error\");\n return;\n }\n if (aborted) return;\n\n const full: StudioConfig = { ...core.createDefaultConfig(), ...staged };\n const rendererOptions: WaveRendererOptions = { respectReducedMotion };\n renderer = new core.WaveRenderer(container, full, rendererOptions);\n const canvas = renderer.renderer.domElement;\n canvas.addEventListener(\"webglcontextlost\", onContextLost, false);\n canvas.addEventListener(\"webglcontextrestored\", onContextRestored, false);\n renderer.start();\n for (const [name, value] of stagedInputs) renderer.setInteractionInput(name, value);\n setState(\"running\");\n options.onReady?.(renderer);\n\n if (poster) {\n // Crossfade only after two frames, so the wave has definitely painted first.\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (!aborted && renderer) poster.fadeOut(fadeMs);\n }),\n );\n }\n }\n\n function probeAndUpgrade(): void {\n if (aborted) return;\n if (webgl === \"auto\" && !hasWebGL()) {\n fallback(\"no-webgl\");\n return;\n }\n void upgrade();\n }\n\n function begin(): void {\n // Permanent-poster gates (checked before any lazy wait or engine fetch).\n if (webgl === \"off\") return; // deliberate poster-only mode — stay \"poster\", no fallback callback\n if (respectSaveData && prefersReducedData()) return fallback(\"save-data\");\n if (respectReducedMotion && reducedMotionBehavior === \"poster\" && prefersReducedMotion()) {\n return fallback(\"reduced-motion\");\n }\n if (lazy && typeof IntersectionObserver !== \"undefined\") {\n io = new IntersectionObserver(\n (entries) => {\n if (entries.some((e) => e.isIntersecting)) {\n io?.disconnect();\n io = null;\n probeAndUpgrade();\n }\n },\n { rootMargin },\n );\n io.observe(container);\n } else {\n probeAndUpgrade();\n }\n }\n\n const handle: WaveHandle = {\n get state() {\n return state;\n },\n get renderer() {\n return renderer;\n },\n snapshot(opts = {}) {\n if (!renderer) return Promise.resolve(null);\n const { type = \"image/webp\", quality, transparent = true, time } = opts;\n return renderer.captureImage(type, transparent, quality, time);\n },\n set(next) {\n if (renderer) {\n renderer.setConfig({ ...renderer.getConfig(), ...next });\n renderer.refreshPlayback(); // setConfig doesn't re-evaluate `paused` on its own\n } else {\n staged = { ...staged, ...next };\n }\n },\n setInteractionInput(name, value) {\n if (renderer) renderer.setInteractionInput(name, value);\n else stagedInputs.set(name, value);\n },\n play() {\n if (renderer) {\n renderer.getConfig().paused = false;\n renderer.refreshPlayback();\n } else {\n staged.paused = false;\n }\n },\n pause() {\n if (renderer) {\n renderer.getConfig().paused = true;\n renderer.refreshPlayback();\n } else {\n staged.paused = true;\n }\n },\n destroy() {\n aborted = true;\n io?.disconnect();\n io = null;\n clearTimeout(lostTimer);\n teardownRenderer();\n poster?.remove();\n },\n };\n\n begin();\n return handle;\n}\n\n/**\n * Mount a self-optimizing wave into a container: shows a poster immediately, then — lazily, and only\n * when the browser can actually run it — fetches the engine, builds the renderer, and crossfades in.\n * Falls back to the poster on no-WebGL / save-data / reduced-motion / context-loss / load errors.\n * No static three import: the engine arrives via a dynamic import, so the shell stays tiny.\n */\nexport function createWave(\n container: HTMLElement,\n config: Partial<StudioConfig> = {},\n options: WaveOptions = {},\n): WaveHandle {\n return createWaveImpl(\n options.loadCore ?? (() => import(\"../core-loader\")),\n container,\n config,\n options,\n );\n}\n\n/** The drop-in embed contract: an alias of {@link createWave}. */\nexport const mountWave = createWave;\n"],"mappings":"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"}
|