@wave3d/core 0.5.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","names":[],"sources":["../../src/config/model.ts"],"sourcesContent":["/**\n * Configuration schema for the wave: a flat sheet displaced by noise (X/Z frequency\n * + amount) then twisted by three axis-rotations (twistFrequency + twistPower per\n * axis), then scaled / rotated / positioned. Plain JSON — doubles as the save-state\n * format.\n */\n\nimport { clamp, clamp01 } from \"../util/math\";\n\nexport const MAX_COLORS = 8;\nexport const MAX_MESH_POINTS = 8;\nexport const MAX_LIGHTS = 8;\nexport const MAX_NOISE_BANDS = 4;\n/** Cap on stacked waves (keeps total geometry bounded — see WaveRenderer segment scaling). */\nexport const MAX_WAVES = 6;\n\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\nexport interface Vec3 {\n x: number;\n y: number;\n z: number;\n}\n\n// \"squared\" = the hero material blend: SrcColor × Zero (framebuffer = fragColor²), which\n// deepens the colours — the faithful default. \"normal\"/\"additive\"/\"multiply\" are authoring\n// overrides (\"multiply\" darkens where waves/background overlap).\nexport type BlendMode = \"squared\" | \"normal\" | \"additive\" | \"multiply\";\n\n/** How the palette is mapped across the surface. */\nexport type BasicGradientType = \"linear\" | \"radial\" | \"conic\";\nexport type GradientType = BasicGradientType | \"mesh\";\n\nexport type BackgroundMode = \"color\" | \"gradient\" | \"image\";\nexport type BackgroundImageFit = \"cover\" | \"contain\" | \"stretch\";\n\n/** How the authored reference frame (FRAME_W × FRAME_H, a 16:9 rectangle centred on cameraTarget)\n * is mapped onto a canvas of a different aspect:\n * - `cover` — fill both axes, crop the overflow. The default and the hero look.\n * - `contain` — fit the whole frame, revealing world beyond it on the long axis.\n * - `width` — always bind on width, so the horizontal composition is identical at every aspect.\n * Above 16:9 this is exactly `cover`; below it, it reveals vertically instead of cropping.\n * - `height` — always bind on height (the mirror of `width`).\n * Pair with `cameraMinVisibleWidth` to land between `cover` and `width`. */\nexport type CameraFit = \"cover\" | \"contain\" | \"width\" | \"height\";\n\n/** Runtime whitelist for {@link CameraFit} (validating imported/serialized configs). */\nexport const CAMERA_FITS: readonly CameraFit[] = [\"cover\", \"contain\", \"width\", \"height\"];\n\n/** What fills the 2D palette texture: the baked hero LUT, our editable stops, or\n * a named built-in map (see PALETTE_MAPS). Any string is allowed for forward-compat. */\nexport type PaletteSource = \"hero\" | \"stops\" | (string & {});\n\n/** A positionable light. `position` lives in the same 3D space as the wave. */\nexport interface LightConfig {\n position: Vec3;\n color: string;\n intensity: number;\n}\n\n/** A default light; pass overrides for added fill lights. */\nexport function createLight(\n position: Vec3 = { x: 300, y: 500, z: 800 },\n intensity = 1,\n): LightConfig {\n return { position: { ...position }, color: \"#ffffff\", intensity };\n}\n\n/** Where the first light lands when you engage lights from an empty scene. Shared by the\n * \"drag in 3D\" control and the camera-rig minimap, which previews a light marker here so the\n * rig always shows the light — even before one has been explicitly added. */\nexport const DEFAULT_LIGHT_POSITION: Vec3 = { x: 800, y: 900, z: 1100 };\n\n/**\n * A noise band: inside a rectangular uv region (startX..endX along the length,\n * startY..endY across the width, softened by `feather`), the fiber streaks are\n * overridden — strength, frequency (density), colourAttenuation (how much the local\n * colour suppresses them), and the end-weighting parabolaPower. Lets the fibers vary\n * per region instead of uniform.\n */\nexport interface NoiseBand {\n startX: number;\n endX: number;\n startY: number;\n endY: number;\n feather: number;\n strength: number;\n frequency: number;\n colorAttenuation: number;\n parabolaPower: number;\n}\n\n/** A default band: a strong, coarse streak region over the first half. */\nexport function createNoiseBand(): NoiseBand {\n return {\n startX: 0.0,\n endX: 0.5,\n startY: 0.0,\n endY: 1.0,\n feather: 0.3,\n strength: 1.0,\n frequency: 220,\n colorAttenuation: 0.0,\n parabolaPower: 2.0,\n };\n}\n\n/** One gradient stop: a colour at a normalized position (0–1) across the width. */\nexport interface ColorStop {\n color: string;\n pos: number;\n}\n\n/** One colour influence point in the 2D mesh-gradient field. */\nexport interface MeshGradientPoint {\n color: string;\n /** Horizontal UV position (0–1). */\n x: number;\n /** Vertical UV position (0–1). */\n y: number;\n /** Relative reach of this point's colour field. */\n influence: number;\n}\n\n/** Build evenly-spaced stops from a plain list of colours. */\nexport function makeStops(colors: string[]): ColorStop[] {\n const n = colors.length;\n return colors.map((color, i) => ({ color, pos: n > 1 ? i / (n - 1) : 0 }));\n}\n\n/** A balanced iOS-style field shown the first time Mesh is selected. */\nexport function createDefaultMeshPoints(): MeshGradientPoint[] {\n return [\n { color: \"#5e5ce6\", x: 0.08, y: 0.12, influence: 0.78 },\n { color: \"#64d2ff\", x: 0.88, y: 0.08, influence: 0.72 },\n { color: \"#ff375f\", x: 0.12, y: 0.88, influence: 0.72 },\n { color: \"#ff9f0a\", x: 0.9, y: 0.86, influence: 0.78 },\n { color: \"#bf5af2\", x: 0.5, y: 0.48, influence: 0.58 },\n ];\n}\n\n/**\n * A single wave: a COMPLETE, self-contained wave — its own shape, twist, colour, finish,\n * transform and blend. Stacking waves composites independent waves; there is no shared\n * \"base wave\" any more, so nothing is duplicated between a global section and the waves.\n * Field names mirror the legacy top-level wave fields so migration + the per-section helpers\n * (normalizeWaveColour, randomize*) map 1:1.\n */\nexport interface WaveConfig {\n // Colour & gradient\n palette: ColorStop[];\n gradientType: GradientType;\n gradientAngle: number;\n gradientShift: number;\n meshGradientPoints: MeshGradientPoint[];\n meshGradientSoftness: number;\n usePaletteTexture: boolean;\n paletteSource: PaletteSource;\n paletteImageUrl?: string;\n paletteVideoUrl?: string;\n paletteTextureScale: Vec2;\n paletteTextureOffset: Vec2;\n paletteTextureRotation: number;\n /** Palette-offset drift per second (animates colour independently of the geometry; 0 = static).\n * Applies to any texture palette (not mesh / procedural stops). */\n paletteDriftX: number;\n paletteDriftY: number;\n paletteEdgeColor: string;\n paletteEdgeAmount: number;\n hueShift: number;\n colorContrast: number;\n colorSaturation: number;\n // Surface finish\n fiberCount: number;\n fiberStrength: number;\n noiseBands: NoiseBand[];\n texture: number;\n creaseLight: number;\n creaseSharpness: number;\n creaseSoftness: number;\n sheen: number;\n roundness: number;\n /** Thin-film / holographic hue response that shifts with view angle (0 = off). */\n iridescence: number;\n edgeFade: number;\n /** Softness of the ribbon's long edges (smoothstep width across uv.y). 0.1 = the original\n * hardcoded value; smaller = razor-crisp graphic ribbons, larger = soft vapor. */\n edgeFeather: number;\n /** Depth tint (solid theme): fade far fragments toward depthTintColor for atmospheric\n * separation in multi-wave stacks (0 = off). */\n depthTint: number;\n depthTintColor: string;\n // Displacement + twist (the wave shape)\n displaceFrequency: Vec2;\n displaceAmount: number;\n /** Optional 2nd displacement octave: finer ripples riding on the broad swell (amount 0 = off). */\n detailFrequency: number;\n detailAmount: number;\n twistFrequency: Vec3;\n twistPower: Vec3;\n twistMotion?: boolean;\n // Material (\"solid\" surface vs \"wireframe\" line shader)\n theme?: \"solid\" | \"wireframe\";\n lineAmount?: number;\n lineThickness?: number;\n lineDerivativePower?: number;\n maxWidth?: number;\n // Transform (absolute — no shared base to offset from)\n position: Vec3;\n rotation: Vec3;\n scale: Vec3;\n // Compositing\n blendMode: BlendMode;\n /** Absolute animation speed for this wave (legacy global speed × per-layer multiplier). */\n speed: number;\n /** Overall opacity of this wave. */\n opacity: number;\n /** Phase/seed so waves don't move in lockstep. */\n seed: number;\n /** Optional per-wave interactivity: how THIS wave reacts to the shared pointer + inputs (hover\n * field, click ripples, param bindings). ABSENT = this wave is inert / byte-identical. */\n interaction?: WaveInteractionConfig;\n}\n\n// ---------------------------------------------------------------------------------------------\n// Interactivity layer (optional, additive, default-off). Split by concern: the SHARED inputs (one\n// cursor + scroll + smoothing/touch) and scene-param effects live on SceneConfig.interaction; each\n// wave's own RESPONSE (hover field, click ripples, param bindings) lives on WaveConfig.interaction.\n// ABSENT blocks mean fully off — the compiled shader and rendered pixels stay byte-identical to a\n// non-interactive wave (the normalizers below run present-only; ensureSceneDefaults never calls them).\n// ---------------------------------------------------------------------------------------------\n\n/** The built-in interaction input names (the open-ended `custom:*` family is handled separately).\n * Kept in sync by hand with the {@link InteractionSource} union below. */\nconst INTERACTION_SOURCE_NAMES = [\n \"scroll\",\n \"hover\",\n \"pointerX\",\n \"pointerY\",\n \"pointerSpeed\",\n \"press\",\n \"scrollVelocity\",\n \"appear\",\n] as const;\n\n/**\n * An interaction INPUT: a normalized signal that can smoothly drive config params through an\n * {@link InteractionBinding}. Every source is exponentially smoothed before it is applied.\n */\nexport type InteractionSource =\n | \"scroll\" // container progress through the viewport, 0 (entering) .. 1 (scrolled past)\n | \"hover\" // smoothed pointer presence over the container, 0..1\n | \"pointerX\" // smoothed pointer X across the container, 0..1; relaxes to 0.5 on leave\n | \"pointerY\" // smoothed pointer Y across the container, 0..1; relaxes to 0.5 on leave\n | \"pointerSpeed\" // normalized smoothed pointer speed, 0..1\n | \"press\" // pointer button / touch held, smoothed 0..1\n | \"scrollVelocity\" // normalized smoothed |d(scroll progress)/dt|, 0..1\n | \"appear\" // one-shot 0→1 latch on first visibility (entrance choreography)\n | `custom:${string}`; // developer-fed each frame via setInteractionInput(name, value)\n\n/** Per-WAVE params a binding may drive. Single source of truth for WAVE_APPLIERS in\n * renderer/interaction.ts (checked via `satisfies`) and validated by normalizeWaveInteraction. */\nconst WAVE_TARGET_NAMES = [\n \"displaceAmount\",\n \"detailAmount\",\n \"twistPowerX\",\n \"twistPowerY\",\n \"twistPowerZ\",\n \"twistFrequencyX\",\n \"twistFrequencyY\",\n \"twistFrequencyZ\",\n \"hueShift\",\n \"gradientShift\",\n \"colorSaturation\",\n \"opacity\",\n \"lineThickness\",\n \"lineAmount\",\n \"fiberStrength\",\n \"sheen\",\n \"iridescence\",\n \"positionX\",\n \"positionY\",\n] as const;\n/** A per-wave param a {@link WaveInteractionBinding} can drive. */\nexport type WaveInteractionTarget = (typeof WAVE_TARGET_NAMES)[number];\n\n/** SCENE params a binding may drive (post / camera / time — shared, not per wave). Single source of\n * truth for SCENE_APPLIERS in renderer/interaction.ts, validated by normalizeSceneInteraction. */\nconst SCENE_TARGET_NAMES = [\"timeOffset\", \"cameraZoom\", \"blur\", \"grain\"] as const;\n/** A scene-level param a {@link SceneInteractionBinding} can drive. */\nexport type SceneInteractionTarget = (typeof SCENE_TARGET_NAMES)[number];\n\n/** Shared fields of an input→param binding: per frame `value = mix(from ?? authoredBase, to,\n * smoothedSource)`, written straight to uniforms — never mutates config, so any refresh restores\n * the authored base (removal needs no undo step). */\ninterface InteractionBindingBase {\n /** The input signal driving this binding. */\n source: InteractionSource;\n /** Value at source = 0. OMITTED = the authored base value, so at rest the authored look shows. */\n from?: number;\n /** Value at source = 1. */\n to: number;\n /** Exponential smoothing time constant, seconds (default 0.25); also shapes the `appear` ramp. */\n smoothing?: number;\n}\n/** A binding on a wave, driving one of that wave's params. */\nexport interface WaveInteractionBinding extends InteractionBindingBase {\n target: WaveInteractionTarget;\n}\n/** A scene-level binding, driving a shared scene param. */\nexport interface SceneInteractionBinding extends InteractionBindingBase {\n target: SceneInteractionTarget;\n}\n\n/** Hover pointer-field: localized effects that follow the cursor over this wave. Present ⇒ the\n * POINTER_FX shader path compiles for this wave; an absent effect is 0 (inert). */\nexport interface WaveHoverConfig {\n /** Local churn-octave amplitude near the cursor — the wave agitates under the pointer. The studio\n * defaults this positive when you enable a hover field, so a fresh hover reacts out of the box. */\n agitate?: number;\n /** Membrane push/pull: a smooth dome at the cursor that swells toward you (repel, +) or dents away\n * (attract, −), carried by the sprung field so it drags like a poke under fabric. World units;\n * 0 = off. */\n push?: number;\n /** Drag-wake: while the cursor moves, the surface just BEHIND it is pulled into a trailing trough\n * that heals once you stop; scales with pointer speed. World units; 0 = off. */\n wake?: number;\n /** 0..1 — wireframe strands taper to hairlines; solid gains local translucency. */\n thin?: number;\n /** Local hue rotation near the cursor, degrees. */\n hueShift?: number;\n /** Local brightness lift near the cursor, -1..1. */\n lighten?: number;\n /** Pointer-follow smoothing for THIS wave's hover field, seconds — how quickly the swell trails\n * the cursor. Vary it across a stack so strands lag at different rates (a parallax drag).\n * Default 0.12. */\n smoothing?: number;\n}\n\n/** Click / touch pointer-field: what a tap or click on this wave triggers. */\nexport interface WavePressConfig {\n /** Click-ripple amplitude; 0 keeps this wave's POINTER_RIPPLES path uncompiled. */\n ripple?: number;\n}\n\n/** Per-wave interactivity: this wave's own reaction to the shared pointer + inputs. ABSENT ⇒ inert. */\nexport interface WaveInteractionConfig {\n /** Hover field (cursor-follow agitation / thinning / hue-lighten). */\n hover?: WaveHoverConfig;\n /** Click & touch (ripples radiating from a tap/click on this wave). */\n press?: WavePressConfig;\n /** Input→param bindings driving THIS wave's params (any source, incl. scroll / hover / custom). */\n bindings?: WaveInteractionBinding[];\n}\n\n/** Scene-level interactivity: the SHARED inputs (one cursor + scroll, touch) plus bindings that\n * drive shared scene params. Pointer-follow smoothing is per-wave (see WaveHoverConfig.smoothing).\n * ABSENT ⇒ inputs use defaults; `enabled: false` is the master OFF switch for the whole layer. */\nexport interface SceneInteractionConfig {\n /** Master switch for the whole interaction layer. Default true (only `false` turns it all off). */\n enabled?: boolean;\n /** Pointer falloff radius, as a fraction of viewport height. Default 0.3. */\n radius?: number;\n /** Ribbon flow (0..1, default 0.8): stretch the pointer footprint along each wave's own length axis\n * so the influence reaches ALONG the ribbon instead of as a circular screen disc. On by default;\n * set 0 for the plain circle. Scene-level (shared like `radius`); each wave uses its own length\n * tangent. Only affects waves that already react to the cursor (non-interactive waves are inert). */\n ribbonFlow?: number;\n /** Follow coarse (touch) pointers. Default false — touch is ignored unless this is true. */\n touch?: boolean;\n /** Input→param bindings driving SCENE params (timeOffset, cameraZoom, blur, grain). */\n bindings?: SceneInteractionBinding[];\n}\n\n/**\n * Scene-level settings shared by every wave: output/background/camera/lights, the post-fx\n * pass (grain/blur), playback, quality, and the whole-composition mirror. Everything that\n * describes an individual wave lives on WaveConfig instead.\n */\nexport interface SceneConfig {\n background: string;\n transparentBackground: boolean;\n backgroundMode: BackgroundMode;\n backgroundPalette: ColorStop[];\n backgroundGradientType: GradientType;\n backgroundGradientAngle: number;\n backgroundGradientSource: PaletteSource;\n backgroundMeshPoints: MeshGradientPoint[];\n backgroundMeshSoftness: number;\n backgroundImageSource: PaletteSource;\n backgroundImageUrl?: string;\n backgroundVideoUrl?: string;\n backgroundImageFit: BackgroundImageFit;\n backgroundImageZoom: number;\n backgroundImagePosition: Vec2;\n /** Number of stacked waves (kept in sync with waves.length). */\n waveCount: number;\n quality: number;\n dprMax: number;\n paused: boolean;\n /** Noise phase offset — scrubs the noise pattern to pick a still frame. */\n timeOffset?: number;\n /** Seamless-loop period in seconds (0 = off). When >0, the motion is mapped onto a circle in\n * noise space so it repeats exactly every `loopSeconds` — scene-level so a multi-wave stack\n * shares one period and the whole composite loops. */\n loopSeconds?: number;\n introRamp?: boolean;\n showCameraRig: boolean;\n cameraDistance: number;\n cameraZoom: number;\n cameraPosition: Vec3;\n cameraTarget: Vec3;\n /** How the authored reference frame maps onto the canvas when their aspects differ.\n * Default `\"cover\"`. See {@link CameraFit}. */\n cameraFit?: CameraFit;\n /** Floor on how much of the authored frame's WIDTH stays on screen, as a fraction (0..1).\n * A ceiling on zoom applied AFTER {@link cameraFit}, so the two compose instead of fighting:\n * it only ever zooms out, and is inert for fits that already do (`contain`, `width`).\n *\n * This is the narrow-screen crop control. `cameraFit: \"cover\"` binds on height once the canvas\n * is narrower than the 16:9 reference, so a portrait phone (390×844 @ dpr 2) zooms 2.25× and\n * shows only ~26% of the authored width. `0.6` holds 60% of it on screen; `1` is equivalent to\n * `\"width\"`. Default 0 (off) — existing configs frame exactly as before.\n *\n * It clamps the BASE zoom, before the cameraZoom multiplier, which makes the fraction read\n * against your own composition rather than the raw constant: `1` shows exactly the horizontal\n * span you see at 16:9 whatever cameraZoom you authored at, and `0.6` shows 60% of that. */\n cameraMinVisibleWidth?: number;\n /** Film grain amount (post pass). */\n grain: number;\n /** Soft-focus / spin blur amount (post pass). */\n blur: number;\n blurSamples?: number;\n /** Bloom (post pass, UnrealBloomPass). strength 0 removes the pass entirely, so cost and pixels\n * are identical to bloom-off; radius/threshold only take effect once strength > 0. */\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n /** Ordered (Bayer) dithering over the finished composite — a self-contained \"layered\" post\n * shader in the spirit of paper-design/shaders. 0 removes the pass entirely (cost/pixels match\n * dither-off); scale & steps only bite once dither > 0. Runs last, after tone-map + sRGB. */\n dither?: number;\n /** Dither cell size in device pixels (>=1) — larger = chunkier pattern. */\n ditherScale?: number;\n /** Quantization levels per channel (>=2) — lower = heavier posterization. */\n ditherSteps?: number;\n /** Volumetric light streaks (innerLight) scattered from the bright wave toward a light point\n * (innerLightX/Y in UV). 0 removes the pass; density/decay/centre only bite once innerLight > 0.\n * Scene-zone (scatters the raw wave, like bloom). */\n innerLight?: number;\n innerLightDensity?: number;\n innerLightDecay?: number;\n innerLightX?: number;\n innerLightY?: number;\n /** Halftone: a rotated dot screen (dot size scales with local brightness) over the final image.\n * 0 removes the pass; cell/angle only bite once halftone > 0. Finish-zone stylization. */\n halftone?: number;\n halftoneCell?: number;\n halftoneAngle?: number;\n /** Heatmap recolour (luminance → thermal palette). 0 removes the pass. Finish-zone. */\n heatmap?: number;\n /** Paper-texture overlay (fibrous substrate shading). 0 removes the pass; scale = grain size. */\n paperTexture?: number;\n paperTextureScale?: number;\n /** CMYK halftone (four rotated dot screens). 0 removes the pass; cell = dot size px. */\n halftoneCmyk?: number;\n halftoneCmykCell?: number;\n /** Base ambient light level (0–1). */\n ambient: number;\n lights: LightConfig[];\n /** Mirror the whole composition on screen (world-space flip). */\n mirrorH: boolean;\n mirrorV: boolean;\n /** Shared interaction inputs (one cursor + scroll) + scene-param bindings. Per-wave response\n * lives on each WaveConfig.interaction. ABSENT = defaults; `enabled:false` disables the layer. */\n interaction?: SceneInteractionConfig;\n}\n\n/** The full save-state: scene settings + one or more complete waves. */\nexport interface StudioConfig extends SceneConfig {\n waves: WaveConfig[];\n}\n\n/** Spread a base wave into `count` overlapping waves — each with a slightly varied hue, width,\n * speed, phase, vertical offset and roll so a stack reads as one composition. `count === 1`\n * returns the base unchanged. Used to author multi-wave presets. */\nexport function makeWaveSpread(base: WaveConfig, count: number): WaveConfig[] {\n if (count <= 1) return [structuredClone(base)];\n const out: WaveConfig[] = [];\n for (let i = 0; i < count; i++) {\n const f = i / (count - 1);\n const w = structuredClone(base);\n w.opacity = 1.0 - f * 0.3;\n w.hueShift = base.hueShift + i * 18;\n w.scale = { x: base.scale.x, y: base.scale.y * (1 - f * 0.2), z: base.scale.z };\n w.speed = base.speed * (1 + f * 0.15);\n w.seed = i * 3.3;\n w.position = {\n x: base.position.x,\n y: base.position.y + (f - 0.5) * 1.5,\n z: base.position.z - i * 0.8,\n };\n w.rotation = { x: base.rotation.x, y: base.rotation.y, z: base.rotation.z + i * 20 };\n out.push(w);\n }\n return out;\n}\n\n/** The hero wave (a single complete wave) — the base for the default config and most presets. */\nfunction defaultWave(): WaveConfig {\n return {\n // The hero palette: a periwinkle tip/edge, a dominant orange core, then coral → magenta →\n // pink, with a violet twist tip. gradientShift warps it to mimic a baked 2D palette texture.\n palette: [\n { color: \"#8e9dff\", pos: 0 }, // periwinkle (blue tip/edge)\n { color: \"#c98fd0\", pos: 0.14 }, // lavender transition\n { color: \"#ff9326\", pos: 0.3 }, // orange (rising)\n { color: \"#fd8108\", pos: 0.52 }, // orange core\n { color: \"#fb7a36\", pos: 0.64 }, // orange-coral (keeps orange dominant)\n { color: \"#d24ecc\", pos: 0.78 }, // true magenta (hue ~303, not pink)\n { color: \"#e95cae\", pos: 0.9 }, // pink-magenta\n { color: \"#9b6ae0\", pos: 1.0 }, // violet (twist tip)\n ],\n gradientType: \"linear\",\n gradientAngle: 90, // 90° = the gradient runs ALONG the length (uv.x)\n gradientShift: 0.15,\n meshGradientPoints: createDefaultMeshPoints(),\n meshGradientSoftness: 0.62,\n usePaletteTexture: true, // default to the baked hero LUT\n paletteSource: \"hero\",\n paletteTextureScale: { x: 1, y: 1 },\n paletteTextureOffset: { x: 0, y: 0 },\n paletteTextureRotation: 0,\n paletteDriftX: 0,\n paletteDriftY: 0,\n paletteEdgeColor: \"#8e9dff\",\n paletteEdgeAmount: 0.3,\n hueShift: -1.81, // hero colorHueShift ≈ -1.81°\n colorContrast: 1.0,\n colorSaturation: 1.15,\n // Hero fibers: the surfaceColor fragment hardcodes freq 600 / strength 0.2; the line* fields\n // feed the wireframe theme (unused by the solid hero).\n fiberCount: 600,\n fiberStrength: 0.2,\n noiseBands: [],\n texture: 0,\n creaseLight: 0.6,\n creaseSharpness: 0.589,\n creaseSoftness: 1.0,\n // sheen 0 + roundness 0: the ortho crop makes crease low, so the hero look comes from the\n // SrcColor² blend + the palette, not the derivative white-lift.\n sheen: 0.0,\n roundness: 0.0,\n iridescence: 0,\n edgeFade: 0.04,\n edgeFeather: 0.1, // the original hardcoded ribbon-edge softness\n depthTint: 0,\n depthTintColor: \"#0a2540\",\n // Hero deformation on the native 400-unit folded() geometry.\n displaceFrequency: { x: 0.003234, y: 0.00799 },\n displaceAmount: 6.051,\n detailFrequency: 0.04, // finer than the base swell; only bites once detailAmount > 0\n detailAmount: 0,\n // Small twist frequencies + high powers — a gentle twist; the drama is the ortho crop.\n twistFrequency: { x: -0.055, y: 0.077, z: -0.518 },\n twistPower: { x: 3.95, y: 5.85, z: 6.33 },\n twistMotion: false,\n theme: \"solid\",\n lineAmount: 425, // wireframe-theme line params (defaults)\n lineThickness: 1,\n lineDerivativePower: 0.95,\n maxWidth: 1232,\n // Hero mesh transform at FULL scale (the ortho camera frames in pixels).\n position: { x: -24.3, y: -56.4, z: -11.1 },\n rotation: { x: -9.14, y: -16.25, z: -161.32 },\n scale: { x: 10, y: 10, z: 7 },\n blendMode: \"squared\", // the hero squaring blend (SrcColor²)\n speed: 0.04, // hero speed: 4e-5 vs ms-time ≈ 0.04/s\n opacity: 1,\n seed: 0,\n };\n}\n\n/** A fresh default wave (the hero wave as one complete wave). */\nexport function makeWave(): WaveConfig {\n return defaultWave();\n}\n\n/** Resize `waves` to match `waveCount`. New waves CLONE the last one (inherit every\n * property of the preceding wave); extras are dropped. */\nexport function resizeWaves(config: StudioConfig): void {\n const target = Math.max(1, Math.round(config.waveCount) || 1);\n if (!Array.isArray(config.waves) || config.waves.length === 0) {\n config.waves = [makeWave()];\n }\n while (config.waves.length < target) {\n config.waves.push(structuredClone(config.waves[config.waves.length - 1]));\n }\n while (config.waves.length > target) config.waves.pop();\n config.waveCount = config.waves.length;\n}\n\n/** The default studio config: the hero wave + its scene, in the canonical wave model. */\nexport function createDefaultConfig(): StudioConfig {\n return {\n background: \"#ffffff\",\n transparentBackground: true,\n backgroundMode: \"color\",\n backgroundPalette: makeStops([\"#0a2540\", \"#425466\", \"#7a73ff\", \"#f6f9fc\"]),\n backgroundGradientType: \"linear\",\n backgroundGradientAngle: 135,\n backgroundGradientSource: \"stops\",\n backgroundMeshPoints: createDefaultMeshPoints(),\n backgroundMeshSoftness: 0.62,\n backgroundImageSource: \"vaporwave\",\n backgroundImageFit: \"cover\",\n backgroundImageZoom: 1,\n backgroundImagePosition: { x: 0, y: 0 },\n waveCount: 1,\n quality: 1,\n dprMax: 2,\n paused: false,\n timeOffset: 0, // noise phase (scrub to pick a still)\n introRamp: true, // ease the animation in over ~1s on load (skipped in dev; see WaveRenderer.updateTime)\n showCameraRig: false,\n // The hero camera: ORTHOGRAPHIC at (100,0,5000) looking at the origin. The mesh is ×10 so\n // the wave overflows the frame and only the twist shows. cameraZoom is a user multiplier on\n // the responsive base zoom (1 = the hero crop); cameraTarget pans the look-at to the twist.\n cameraDistance: 5001,\n cameraPosition: { x: 100, y: 0, z: 5000 },\n cameraTarget: { x: -44, y: -250, z: 0 },\n cameraZoom: 1.0,\n cameraFit: \"cover\",\n cameraMinVisibleWidth: 0, // off — see the field docs for the narrow-screen crop control\n // Post (one pass over the whole composite): hero grain 1.1, blur 0.02.\n grain: 1.1,\n blur: 0.02,\n blurSamples: 6,\n dither: 0, // off by default — the hero look is unchanged (the pass isn't inserted)\n ditherScale: 2,\n ditherSteps: 4,\n innerLight: 0,\n innerLightDensity: 0.5,\n innerLightDecay: 0.95,\n innerLightX: 0.5,\n innerLightY: 0.15,\n halftone: 0,\n halftoneCell: 6,\n halftoneAngle: 0.4,\n heatmap: 0,\n paperTexture: 0,\n paperTextureScale: 2,\n halftoneCmyk: 0,\n halftoneCmykCell: 6,\n ambient: 0.45,\n lights: [], // hero has no lights — colour is the palette + the SrcColor² blend\n mirrorH: false,\n mirrorV: false,\n waves: [defaultWave()],\n };\n}\n\n/** Clamp/backfill a single wave's colour + palette fields (legacy `string[]` palettes become\n * ColorStop[]; mesh points + texture transform are clamped). */\nfunction normalizeWaveColour(config: WaveConfig): void {\n const p = config.palette as unknown as Array<string | ColorStop>;\n if (p.length > 0 && typeof p[0] === \"string\") {\n config.palette = makeStops(p as string[]);\n }\n if (\n config.gradientType !== \"radial\" &&\n config.gradientType !== \"conic\" &&\n config.gradientType !== \"mesh\" &&\n config.gradientType !== \"linear\"\n ) {\n config.gradientType = \"linear\";\n }\n const rawMeshPoints = config.meshGradientPoints as MeshGradientPoint[] | undefined;\n if (!Array.isArray(rawMeshPoints) || rawMeshPoints.length < 2) {\n config.meshGradientPoints = createDefaultMeshPoints();\n } else {\n const defaults = createDefaultMeshPoints();\n config.meshGradientPoints = rawMeshPoints.slice(0, MAX_MESH_POINTS).map((point, index) => {\n const fallback = defaults[index] ?? defaults[defaults.length - 1];\n const x = Number(point.x);\n const y = Number(point.y);\n const influence = Number(point.influence);\n return {\n color: typeof point.color === \"string\" ? point.color : fallback.color,\n x: clamp01(Number.isFinite(x) ? x : fallback.x),\n y: clamp01(Number.isFinite(y) ? y : fallback.y),\n influence: clamp(Number.isFinite(influence) ? influence : fallback.influence, 0.15, 1.5),\n };\n });\n }\n if (!Number.isFinite(config.meshGradientSoftness)) config.meshGradientSoftness = 0.62;\n config.meshGradientSoftness = clamp01(config.meshGradientSoftness);\n if (!config.paletteTextureScale) config.paletteTextureScale = { x: 1, y: 1 };\n if (!config.paletteTextureOffset) config.paletteTextureOffset = { x: 0, y: 0 };\n config.paletteTextureScale.x = clamp(Number(config.paletteTextureScale.x) || 1, 0.1, 8);\n config.paletteTextureScale.y = clamp(Number(config.paletteTextureScale.y) || 1, 0.1, 8);\n config.paletteTextureOffset.x = clamp(Number(config.paletteTextureOffset.x) || 0, -4, 4);\n config.paletteTextureOffset.y = clamp(Number(config.paletteTextureOffset.y) || 0, -4, 4);\n config.paletteTextureRotation = clamp(Number(config.paletteTextureRotation) || 0, -180, 180);\n}\n\n/** Backfill background styling for states saved before gradient/image backgrounds existed. */\nexport function normalizeBackground(config: StudioConfig): void {\n if (\n config.backgroundMode !== \"gradient\" &&\n config.backgroundMode !== \"image\" &&\n config.backgroundMode !== \"color\"\n ) {\n config.backgroundMode = \"color\";\n }\n const palette = config.backgroundPalette as unknown as Array<string | ColorStop> | undefined;\n if (!palette || palette.length < 2) {\n config.backgroundPalette = makeStops([\"#0a2540\", \"#425466\", \"#7a73ff\", \"#f6f9fc\"]);\n } else if (typeof palette[0] === \"string\") {\n config.backgroundPalette = makeStops(palette as string[]);\n }\n if (\n config.backgroundGradientType !== \"radial\" &&\n config.backgroundGradientType !== \"conic\" &&\n config.backgroundGradientType !== \"mesh\" &&\n config.backgroundGradientType !== \"linear\"\n ) {\n config.backgroundGradientType = \"linear\";\n }\n const bgMesh = config.backgroundMeshPoints as MeshGradientPoint[] | undefined;\n if (!Array.isArray(bgMesh) || bgMesh.length < 2) {\n config.backgroundMeshPoints = createDefaultMeshPoints();\n }\n if (!Number.isFinite(config.backgroundMeshSoftness)) config.backgroundMeshSoftness = 0.62;\n config.backgroundMeshSoftness = clamp01(config.backgroundMeshSoftness);\n if (typeof config.backgroundGradientAngle !== \"number\") config.backgroundGradientAngle = 135;\n if (typeof config.backgroundGradientSource !== \"string\")\n config.backgroundGradientSource = \"stops\";\n if (typeof config.backgroundImageSource !== \"string\") config.backgroundImageSource = \"vaporwave\";\n if (\n config.backgroundImageFit !== \"contain\" &&\n config.backgroundImageFit !== \"stretch\" &&\n config.backgroundImageFit !== \"cover\"\n ) {\n config.backgroundImageFit = \"cover\";\n }\n if (typeof config.backgroundImageZoom !== \"number\") config.backgroundImageZoom = 1;\n config.backgroundImageZoom = clamp(config.backgroundImageZoom, 0.1, 8);\n if (!config.backgroundImagePosition) config.backgroundImagePosition = { x: 0, y: 0 };\n if (typeof config.backgroundImagePosition.x !== \"number\") config.backgroundImagePosition.x = 0;\n if (typeof config.backgroundImagePosition.y !== \"number\") config.backgroundImagePosition.y = 0;\n config.backgroundImagePosition.x = clamp(config.backgroundImagePosition.x, -100, 100);\n config.backgroundImagePosition.y = clamp(config.backgroundImagePosition.y, -100, 100);\n}\n\n/** Backfill camera position/target for states saved before they existed. */\nexport function ensureCamera(config: StudioConfig): void {\n if (!config.cameraPosition)\n config.cameraPosition = { x: 0, y: 0, z: config.cameraDistance ?? 62 };\n if (!config.cameraTarget) config.cameraTarget = { x: 0, y: 0, z: 0 };\n if (typeof config.cameraZoom !== \"number\") config.cameraZoom = 1;\n // Framing policy: absent → the historical cover framing, so every saved config/preset that\n // predates these fields reproduces byte-identically.\n if (!CAMERA_FITS.includes(config.cameraFit as CameraFit)) config.cameraFit = \"cover\";\n config.cameraMinVisibleWidth =\n typeof config.cameraMinVisibleWidth === \"number\"\n ? clamp(config.cameraMinVisibleWidth, 0, 1)\n : 0;\n}\n\n/** Backfill/repair a wave so the renderer can consume it (covers partial wave-model JSON). */\nexport function normalizeWave(s: WaveConfig): void {\n normalizeWaveColour(s);\n if (typeof s.gradientAngle !== \"number\") s.gradientAngle = 90;\n if (typeof s.gradientShift !== \"number\") s.gradientShift = 0.15;\n if (typeof s.usePaletteTexture !== \"boolean\") s.usePaletteTexture = true;\n if (typeof s.paletteSource !== \"string\") s.paletteSource = \"hero\";\n if (typeof s.paletteEdgeColor !== \"string\") s.paletteEdgeColor = \"#8e9dff\";\n if (typeof s.paletteEdgeAmount !== \"number\") s.paletteEdgeAmount = 0.3;\n if (typeof s.paletteDriftX !== \"number\") s.paletteDriftX = 0;\n if (typeof s.paletteDriftY !== \"number\") s.paletteDriftY = 0;\n if (typeof s.hueShift !== \"number\") s.hueShift = 0;\n if (typeof s.colorContrast !== \"number\") s.colorContrast = 1;\n if (typeof s.colorSaturation !== \"number\") s.colorSaturation = 1;\n if (typeof s.fiberCount !== \"number\") s.fiberCount = 600;\n if (typeof s.fiberStrength !== \"number\") s.fiberStrength = 0.2;\n if (!Array.isArray(s.noiseBands)) s.noiseBands = [];\n if (typeof s.texture !== \"number\") s.texture = 0;\n if (typeof s.creaseLight !== \"number\") s.creaseLight = 0.6;\n if (typeof s.creaseSharpness !== \"number\") s.creaseSharpness = 0.589;\n if (typeof s.creaseSoftness !== \"number\") s.creaseSoftness = 1;\n if (typeof s.sheen !== \"number\") s.sheen = 0;\n if (typeof s.roundness !== \"number\") s.roundness = 0;\n if (typeof s.iridescence !== \"number\") s.iridescence = 0;\n if (typeof s.edgeFade !== \"number\") s.edgeFade = 0.04;\n if (typeof s.edgeFeather !== \"number\") s.edgeFeather = 0.1;\n if (typeof s.depthTint !== \"number\") s.depthTint = 0;\n if (typeof s.depthTintColor !== \"string\") s.depthTintColor = \"#0a2540\";\n if (!s.displaceFrequency) s.displaceFrequency = { x: 0.003234, y: 0.00799 };\n if (typeof s.displaceAmount !== \"number\") s.displaceAmount = 6.051;\n if (typeof s.detailFrequency !== \"number\") s.detailFrequency = 0.04;\n if (typeof s.detailAmount !== \"number\") s.detailAmount = 0;\n if (!s.twistFrequency) s.twistFrequency = { x: -0.055, y: 0.077, z: -0.518 };\n if (!s.twistPower) s.twistPower = { x: 3.95, y: 5.85, z: 6.33 };\n if (typeof s.theme !== \"string\") s.theme = \"solid\";\n if (typeof s.lineAmount !== \"number\") s.lineAmount = 425;\n if (typeof s.lineThickness !== \"number\") s.lineThickness = 1;\n if (typeof s.lineDerivativePower !== \"number\") s.lineDerivativePower = 0.95;\n if (typeof s.maxWidth !== \"number\") s.maxWidth = 1232;\n if (!s.position) s.position = { x: 0, y: 0, z: 0 };\n if (!s.rotation) s.rotation = { x: 0, y: 0, z: 0 };\n if (!s.scale) s.scale = { x: 10, y: 10, z: 7 };\n if (typeof s.blendMode !== \"string\") s.blendMode = \"squared\";\n if (typeof s.speed !== \"number\") s.speed = 0.04;\n if (typeof s.opacity !== \"number\") s.opacity = 1;\n if (typeof s.seed !== \"number\") s.seed = 0;\n if (s.interaction) normalizeWaveInteraction(s); // present-only; absence stays inert\n}\n\n/** Backfill scene-level defaults (background/camera/post/lights/quality/mirror). */\nexport function ensureSceneDefaults(config: StudioConfig): void {\n normalizeBackground(config);\n ensureCamera(config);\n if (typeof config.ambient !== \"number\") config.ambient = 0.45;\n if (!Array.isArray(config.lights)) config.lights = [];\n if (typeof config.quality !== \"number\") config.quality = 1;\n if (typeof config.dprMax !== \"number\") config.dprMax = 2;\n if (typeof config.grain !== \"number\") config.grain = 1.1;\n if (typeof config.blur !== \"number\") config.blur = 0.02;\n if (typeof config.blurSamples !== \"number\") config.blurSamples = 6;\n if (typeof config.bloomStrength !== \"number\") config.bloomStrength = 0;\n if (typeof config.bloomRadius !== \"number\") config.bloomRadius = 0.4;\n if (typeof config.bloomThreshold !== \"number\") config.bloomThreshold = 0.85;\n if (typeof config.dither !== \"number\") config.dither = 0;\n if (typeof config.ditherScale !== \"number\") config.ditherScale = 2;\n if (typeof config.ditherSteps !== \"number\") config.ditherSteps = 4;\n if (typeof config.innerLight !== \"number\") config.innerLight = 0;\n if (typeof config.innerLightDensity !== \"number\") config.innerLightDensity = 0.5;\n if (typeof config.innerLightDecay !== \"number\") config.innerLightDecay = 0.95;\n if (typeof config.innerLightX !== \"number\") config.innerLightX = 0.5;\n if (typeof config.innerLightY !== \"number\") config.innerLightY = 0.15;\n if (typeof config.halftone !== \"number\") config.halftone = 0;\n if (typeof config.halftoneCell !== \"number\") config.halftoneCell = 6;\n if (typeof config.halftoneAngle !== \"number\") config.halftoneAngle = 0.4;\n if (typeof config.heatmap !== \"number\") config.heatmap = 0;\n if (typeof config.paperTexture !== \"number\") config.paperTexture = 0;\n if (typeof config.paperTextureScale !== \"number\") config.paperTextureScale = 2;\n if (typeof config.halftoneCmyk !== \"number\") config.halftoneCmyk = 0;\n if (typeof config.halftoneCmykCell !== \"number\") config.halftoneCmykCell = 6;\n if (typeof config.showCameraRig !== \"boolean\") config.showCameraRig = false;\n if (typeof config.paused !== \"boolean\") config.paused = false;\n if (typeof config.loopSeconds !== \"number\") config.loopSeconds = 0;\n if (typeof config.mirrorH !== \"boolean\") config.mirrorH = false;\n if (typeof config.mirrorV !== \"boolean\") config.mirrorV = false;\n // NOTE: `interaction` (scene + per-wave) is deliberately NOT backfilled — absence is semantically\n // \"off\" and keeps the compiled shader byte-identical. The present-only normalizers below run from\n // ensureStudioConfig / normalizeWave only when a block is actually present.\n}\n\n/** Clamp an untrusted numeric field, falling back to `dflt` when it isn't a finite number. */\nfunction clampNumber(v: unknown, min: number, max: number, dflt: number): number {\n const n = Number(v);\n return Number.isFinite(n) ? clamp(n, min, max) : dflt;\n}\n\n/** True for a valid interaction source string: a built-in name or a non-empty `custom:<name>`. */\nfunction isInteractionSource(v: unknown): v is InteractionSource {\n return (\n typeof v === \"string\" &&\n ((INTERACTION_SOURCE_NAMES as readonly string[]).includes(v) ||\n (v.startsWith(\"custom:\") && v.length > \"custom:\".length))\n );\n}\n\n/** Rebuild an untrusted bindings array into valid bindings for `valid` targets (loaded share-links /\n * presets are untrusted JSON; we validate source/target/to and rebuild clean objects). */\nfunction cleanBindings<T extends string>(\n raw: unknown,\n valid: readonly string[],\n): Array<InteractionBindingBase & { target: T }> {\n const out: Array<InteractionBindingBase & { target: T }> = [];\n if (!Array.isArray(raw)) return out;\n for (const item of raw) {\n if (!item || typeof item !== \"object\") continue;\n const b = item as Record<string, unknown>;\n if (!isInteractionSource(b.source)) continue;\n if (!valid.includes(b.target as string)) continue;\n const to = Number(b.to);\n if (!Number.isFinite(to)) continue;\n const clean: InteractionBindingBase & { target: T } = {\n source: b.source,\n target: b.target as T,\n to,\n };\n if (b.from !== undefined) {\n const f = Number(b.from);\n if (Number.isFinite(f)) clean.from = f;\n }\n if (b.smoothing !== undefined) clean.smoothing = clampNumber(b.smoothing, 0, 2, 0.25);\n out.push(clean);\n }\n return out;\n}\n\n/**\n * Present-only normalizer for a WAVE's interaction block: clamp the hover/press numerics that are\n * present (absent fields stay absent, so the block stays lean and the renderer's defaults apply) and\n * drop bindings with an unknown source/target or a non-finite `to`. NEVER call when the block is\n * absent — absence is inert and byte-identical (normalizeWave gates on presence).\n */\nexport function normalizeWaveInteraction(wave: WaveConfig): void {\n const it = wave.interaction;\n if (!it) return;\n const h = it.hover;\n if (h) {\n if (h.agitate !== undefined) h.agitate = clampNumber(h.agitate, 0, 60, 0);\n if (h.push !== undefined) h.push = clampNumber(h.push, -40, 40, 0);\n if (h.wake !== undefined) h.wake = clampNumber(h.wake, 0, 40, 0);\n if (h.thin !== undefined) h.thin = clampNumber(h.thin, 0, 1, 0);\n if (h.hueShift !== undefined) h.hueShift = clampNumber(h.hueShift, -360, 360, 0);\n if (h.lighten !== undefined) h.lighten = clampNumber(h.lighten, -1, 1, 0);\n if (h.smoothing !== undefined) h.smoothing = clampNumber(h.smoothing, 0, 2, 0.12);\n }\n if (it.press && it.press.ripple !== undefined) {\n it.press.ripple = clampNumber(it.press.ripple, 0, 60, 0);\n }\n if (it.bindings !== undefined) {\n it.bindings = cleanBindings<WaveInteractionTarget>(it.bindings, WAVE_TARGET_NAMES);\n }\n}\n\n/** Present-only normalizer for the SCENE interaction block: clamp the shared pointer inputs and drop\n * invalid scene bindings. NEVER call when the block is absent. */\nexport function normalizeSceneInteraction(config: StudioConfig): void {\n const it = config.interaction;\n if (!it) return;\n if (it.radius !== undefined) it.radius = clampNumber(it.radius, 0.02, 2, 0.3);\n if (it.ribbonFlow !== undefined) it.ribbonFlow = clampNumber(it.ribbonFlow, 0, 1, 0.8);\n if (it.bindings !== undefined) {\n it.bindings = cleanBindings<SceneInteractionTarget>(it.bindings, SCENE_TARGET_NAMES);\n }\n}\n\n/** Normalize an ingested config to the wave model: backfill the scene + every wave, and drop in\n * a default wave if none are present. Idempotent, so it is safe on the renderer's own config as\n * well as freshly loaded save-states / share links. */\nexport function ensureStudioConfig(input: StudioConfig): StudioConfig {\n const config = input;\n ensureSceneDefaults(config);\n if (!Array.isArray(config.waves) || config.waves.length === 0) {\n config.waves = [makeWave()];\n }\n config.waves.forEach(normalizeWave); // each wave's normalizeWave runs normalizeWaveInteraction\n config.waveCount = config.waves.length;\n // Present-only: a config without a scene `interaction` block is left untouched (stays \"off\").\n if (config.interaction) normalizeSceneInteraction(config);\n return config;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.js","names":[],"sources":["../../src/config/model.ts"],"sourcesContent":["/**\n * Configuration schema for the wave: a flat sheet displaced by noise (X/Z frequency\n * + amount) then twisted by three axis-rotations (twistFrequency + twistPower per\n * axis), then scaled / rotated / positioned. Plain JSON — doubles as the save-state\n * format.\n */\n\nimport { clamp, clamp01 } from \"../util/math\";\n\nexport const MAX_COLORS = 8;\nexport const MAX_MESH_POINTS = 8;\nexport const MAX_LIGHTS = 8;\nexport const MAX_NOISE_BANDS = 4;\n/** Cap on stacked waves (keeps total geometry bounded — see WaveRenderer segment scaling). */\nexport const MAX_WAVES = 6;\n\nexport interface Vec2 {\n x: number;\n y: number;\n}\n\nexport interface Vec3 {\n x: number;\n y: number;\n z: number;\n}\n\n// \"squared\" = the hero material blend: SrcColor × Zero (framebuffer = fragColor²), which\n// deepens the colours — the faithful default. \"normal\"/\"additive\"/\"multiply\" are authoring\n// overrides (\"multiply\" darkens where waves/background overlap).\nexport type BlendMode = \"squared\" | \"normal\" | \"additive\" | \"multiply\";\n\n/** How the palette is mapped across the surface. */\nexport type BasicGradientType = \"linear\" | \"radial\" | \"conic\";\nexport type GradientType = BasicGradientType | \"mesh\";\n\nexport type BackgroundMode = \"color\" | \"gradient\" | \"image\";\nexport type BackgroundImageFit = \"cover\" | \"contain\" | \"stretch\";\n\n/** How the authored reference frame (FRAME_W × FRAME_H, a 16:9 rectangle centred on cameraTarget)\n * is mapped onto a canvas of a different aspect:\n * - `cover` — fill both axes, crop the overflow. The default and the hero look.\n * - `contain` — fit the whole frame, revealing world beyond it on the long axis.\n * - `width` — always bind on width, so the horizontal composition is identical at every aspect.\n * Above 16:9 this is exactly `cover`; below it, it reveals vertically instead of cropping.\n * - `height` — always bind on height (the mirror of `width`).\n * Pair with `cameraMinVisibleWidth` to land between `cover` and `width`. */\nexport type CameraFit = \"cover\" | \"contain\" | \"width\" | \"height\";\n\n/** Runtime whitelist for {@link CameraFit} (validating imported/serialized configs). */\nexport const CAMERA_FITS: readonly CameraFit[] = [\"cover\", \"contain\", \"width\", \"height\"];\n\n/** What fills the 2D palette texture: the baked hero LUT, our editable stops, or\n * a named built-in map (see PALETTE_MAPS). Any string is allowed for forward-compat. */\nexport type PaletteSource = \"hero\" | \"stops\" | (string & {});\n\n/** A positionable light. `position` lives in the same 3D space as the wave. */\nexport interface LightConfig {\n position: Vec3;\n color: string;\n intensity: number;\n}\n\n/** A default light; pass overrides for added fill lights. */\nexport function createLight(\n position: Vec3 = { x: 300, y: 500, z: 800 },\n intensity = 1,\n): LightConfig {\n return { position: { ...position }, color: \"#ffffff\", intensity };\n}\n\n/** Where the first light lands when you engage lights from an empty scene. Shared by the\n * \"drag in 3D\" control and the camera-rig minimap, which previews a light marker here so the\n * rig always shows the light — even before one has been explicitly added. */\nexport const DEFAULT_LIGHT_POSITION: Vec3 = { x: 800, y: 900, z: 1100 };\n\n/**\n * A noise band: inside a rectangular uv region (startX..endX along the length,\n * startY..endY across the width, softened by `feather`), the fiber streaks are\n * overridden — strength, frequency (density), colourAttenuation (how much the local\n * colour suppresses them), and the end-weighting parabolaPower. Lets the fibers vary\n * per region instead of uniform.\n */\nexport interface NoiseBand {\n startX: number;\n endX: number;\n startY: number;\n endY: number;\n feather: number;\n strength: number;\n frequency: number;\n colorAttenuation: number;\n parabolaPower: number;\n}\n\n/** A default band: a strong, coarse streak region over the first half. */\nexport function createNoiseBand(): NoiseBand {\n return {\n startX: 0.0,\n endX: 0.5,\n startY: 0.0,\n endY: 1.0,\n feather: 0.3,\n strength: 1.0,\n frequency: 220,\n colorAttenuation: 0.0,\n parabolaPower: 2.0,\n };\n}\n\n/** One gradient stop: a colour at a normalized position (0–1) across the width. */\nexport interface ColorStop {\n color: string;\n pos: number;\n}\n\n/** One colour influence point in the 2D mesh-gradient field. */\nexport interface MeshGradientPoint {\n color: string;\n /** Horizontal UV position (0–1). */\n x: number;\n /** Vertical UV position (0–1). */\n y: number;\n /** Relative reach of this point's colour field. */\n influence: number;\n}\n\n/** Build evenly-spaced stops from a plain list of colours. */\nexport function makeStops(colors: string[]): ColorStop[] {\n const n = colors.length;\n return colors.map((color, i) => ({ color, pos: n > 1 ? i / (n - 1) : 0 }));\n}\n\n/** A balanced iOS-style field shown the first time Mesh is selected. */\nexport function createDefaultMeshPoints(): MeshGradientPoint[] {\n return [\n { color: \"#5e5ce6\", x: 0.08, y: 0.12, influence: 0.78 },\n { color: \"#64d2ff\", x: 0.88, y: 0.08, influence: 0.72 },\n { color: \"#ff375f\", x: 0.12, y: 0.88, influence: 0.72 },\n { color: \"#ff9f0a\", x: 0.9, y: 0.86, influence: 0.78 },\n { color: \"#bf5af2\", x: 0.5, y: 0.48, influence: 0.58 },\n ];\n}\n\n/**\n * A single wave: a COMPLETE, self-contained wave — its own shape, twist, colour, finish,\n * transform and blend. Stacking waves composites independent waves; there is no shared\n * \"base wave\" any more, so nothing is duplicated between a global section and the waves.\n * Field names mirror the legacy top-level wave fields so migration + the per-section helpers\n * (normalizeWaveColour, randomize*) map 1:1.\n */\nexport interface WaveConfig {\n // Colour & gradient\n palette: ColorStop[];\n gradientType: GradientType;\n gradientAngle: number;\n gradientShift: number;\n meshGradientPoints: MeshGradientPoint[];\n meshGradientSoftness: number;\n usePaletteTexture: boolean;\n paletteSource: PaletteSource;\n paletteImageUrl?: string;\n paletteVideoUrl?: string;\n paletteTextureScale: Vec2;\n paletteTextureOffset: Vec2;\n paletteTextureRotation: number;\n /** Palette-offset drift per second (animates colour independently of the geometry; 0 = static).\n * Applies to any texture palette (not mesh / procedural stops). */\n paletteDriftX: number;\n paletteDriftY: number;\n paletteEdgeColor: string;\n paletteEdgeAmount: number;\n hueShift: number;\n colorContrast: number;\n colorSaturation: number;\n // Surface finish\n fiberCount: number;\n fiberStrength: number;\n noiseBands: NoiseBand[];\n texture: number;\n creaseLight: number;\n creaseSharpness: number;\n creaseSoftness: number;\n sheen: number;\n roundness: number;\n /** Thin-film / holographic hue response that shifts with view angle (0 = off). */\n iridescence: number;\n edgeFade: number;\n /** Softness of the ribbon's long edges (smoothstep width across uv.y). 0.1 = the original\n * hardcoded value; smaller = razor-crisp graphic ribbons, larger = soft vapor. */\n edgeFeather: number;\n /** Depth tint (solid theme): fade far fragments toward depthTintColor for atmospheric\n * separation in multi-wave stacks (0 = off). */\n depthTint: number;\n depthTintColor: string;\n // Displacement + twist (the wave shape)\n displaceFrequency: Vec2;\n displaceAmount: number;\n /** Optional 2nd displacement octave: finer ripples riding on the broad swell (amount 0 = off). */\n detailFrequency: number;\n detailAmount: number;\n twistFrequency: Vec3;\n twistPower: Vec3;\n twistMotion?: boolean;\n /** Helix: sweep the ribbon around its OWN length axis, `helixTurns` full turns from end to end.\n * The three twists above can't express this — their axes sit 45°/90° off the length, and\n * `expStep` is a monotone falloff, so nothing they do ever repeats. This angle is periodic in the\n * length coordinate instead, which is what makes a helix (and a DNA ladder) reachable.\n * 0 turns still leaves the block inert unless radius or roll is non-zero. */\n helixTurns?: number;\n /** Carries the WHOLE ribbon around the axis at this radius (world units, pre-scale), keeping its\n * orientation. A narrow ribbon then reads as a single strand — two waves half a turn apart in\n * `helixPhase` are the two strands of a double helix. 0 = off. */\n helixRadius?: number;\n /** Rolls the ribbon's own cross-section about the axis as it advances, as a fraction of\n * `helixTurns` (1 = rolls exactly in step, a rigid twisted ribbon). That swings the ribbon's two\n * long edges onto opposite sides of the axis, so ONE wave becomes a ladder whose edges are both\n * strands — pair it with `rungAmount` for the rungs between them. 0 = off. */\n helixRoll?: number;\n /** Phase offset in degrees — where along the turn the ribbon starts. The per-wave knob that puts\n * a second wave on the opposite side of the same helix (180). */\n helixPhase?: number;\n // Material (\"solid\" surface vs \"wireframe\" line shader)\n theme?: \"solid\" | \"wireframe\";\n lineAmount?: number;\n lineThickness?: number;\n lineDerivativePower?: number;\n /** Wireframe RUNGS: a second line family carved at constant uv.y, so these run ACROSS the ribbon\n * where `lineAmount`'s run along it — the two cross into a ladder. Frequency, like `lineAmount`\n * (rungs ≈ amount / π). 0 = off, and the cross-wise path isn't compiled. */\n rungAmount?: number;\n /** Rung line width, in pixels (screen-space, so it holds at any zoom). */\n rungThickness?: number;\n maxWidth?: number;\n // Transform (absolute — no shared base to offset from)\n position: Vec3;\n rotation: Vec3;\n scale: Vec3;\n // Compositing\n blendMode: BlendMode;\n /** Absolute animation speed for this wave (legacy global speed × per-layer multiplier). */\n speed: number;\n /** Overall opacity of this wave. */\n opacity: number;\n /** Phase/seed so waves don't move in lockstep. */\n seed: number;\n /** Optional per-wave interactivity: how THIS wave reacts to the shared pointer + inputs (hover\n * field, click ripples, param bindings). ABSENT = this wave is inert / byte-identical. */\n interaction?: WaveInteractionConfig;\n}\n\n// ---------------------------------------------------------------------------------------------\n// Interactivity layer (optional, additive, default-off). Split by concern: the SHARED inputs (one\n// cursor + scroll + smoothing/touch) and scene-param effects live on SceneConfig.interaction; each\n// wave's own RESPONSE (hover field, click ripples, param bindings) lives on WaveConfig.interaction.\n// ABSENT blocks mean fully off — the compiled shader and rendered pixels stay byte-identical to a\n// non-interactive wave (the normalizers below run present-only; ensureSceneDefaults never calls them).\n// ---------------------------------------------------------------------------------------------\n\n/** The built-in interaction input names (the open-ended `custom:*` family is handled separately).\n * Kept in sync by hand with the {@link InteractionSource} union below. */\nconst INTERACTION_SOURCE_NAMES = [\n \"scroll\",\n \"hover\",\n \"pointerX\",\n \"pointerY\",\n \"pointerSpeed\",\n \"press\",\n \"scrollVelocity\",\n \"appear\",\n] as const;\n\n/**\n * An interaction INPUT: a normalized signal that can smoothly drive config params through an\n * {@link InteractionBinding}. Every source is exponentially smoothed before it is applied.\n */\nexport type InteractionSource =\n | \"scroll\" // container progress through the viewport, 0 (entering) .. 1 (scrolled past)\n | \"hover\" // smoothed pointer presence over the container, 0..1\n | \"pointerX\" // smoothed pointer X across the container, 0..1; relaxes to 0.5 on leave\n | \"pointerY\" // smoothed pointer Y across the container, 0..1; relaxes to 0.5 on leave\n | \"pointerSpeed\" // normalized smoothed pointer speed, 0..1\n | \"press\" // pointer button / touch held, smoothed 0..1\n | \"scrollVelocity\" // normalized smoothed |d(scroll progress)/dt|, 0..1\n | \"appear\" // one-shot 0→1 latch on first visibility (entrance choreography)\n | `custom:${string}`; // developer-fed each frame via setInteractionInput(name, value)\n\n/** Per-WAVE params a binding may drive. Single source of truth for WAVE_APPLIERS in\n * renderer/interaction.ts (checked via `satisfies`) and validated by normalizeWaveInteraction. */\nconst WAVE_TARGET_NAMES = [\n \"displaceAmount\",\n \"detailAmount\",\n \"twistPowerX\",\n \"twistPowerY\",\n \"twistPowerZ\",\n \"twistFrequencyX\",\n \"twistFrequencyY\",\n \"twistFrequencyZ\",\n \"helixPhase\",\n \"helixTurns\",\n \"helixRadius\",\n \"hueShift\",\n \"gradientShift\",\n \"colorSaturation\",\n \"opacity\",\n \"lineThickness\",\n \"lineAmount\",\n \"fiberStrength\",\n \"sheen\",\n \"iridescence\",\n \"positionX\",\n \"positionY\",\n] as const;\n/** A per-wave param a {@link WaveInteractionBinding} can drive. */\nexport type WaveInteractionTarget = (typeof WAVE_TARGET_NAMES)[number];\n\n/** SCENE params a binding may drive (post / camera / time — shared, not per wave). Single source of\n * truth for SCENE_APPLIERS in renderer/interaction.ts, validated by normalizeSceneInteraction. */\nconst SCENE_TARGET_NAMES = [\"timeOffset\", \"cameraZoom\", \"blur\", \"grain\"] as const;\n/** A scene-level param a {@link SceneInteractionBinding} can drive. */\nexport type SceneInteractionTarget = (typeof SCENE_TARGET_NAMES)[number];\n\n/** Shared fields of an input→param binding: per frame `value = mix(from ?? authoredBase, to,\n * smoothedSource)`, written straight to uniforms — never mutates config, so any refresh restores\n * the authored base (removal needs no undo step). */\ninterface InteractionBindingBase {\n /** The input signal driving this binding. */\n source: InteractionSource;\n /** Value at source = 0. OMITTED = the authored base value, so at rest the authored look shows. */\n from?: number;\n /** Value at source = 1. */\n to: number;\n /** Exponential smoothing time constant, seconds (default 0.25); also shapes the `appear` ramp. */\n smoothing?: number;\n}\n/** A binding on a wave, driving one of that wave's params. */\nexport interface WaveInteractionBinding extends InteractionBindingBase {\n target: WaveInteractionTarget;\n}\n/** A scene-level binding, driving a shared scene param. */\nexport interface SceneInteractionBinding extends InteractionBindingBase {\n target: SceneInteractionTarget;\n}\n\n/** Hover pointer-field: localized effects that follow the cursor over this wave. Present ⇒ the\n * POINTER_FX shader path compiles for this wave; an absent effect is 0 (inert). */\nexport interface WaveHoverConfig {\n /** Local churn-octave amplitude near the cursor — the wave agitates under the pointer. The studio\n * defaults this positive when you enable a hover field, so a fresh hover reacts out of the box. */\n agitate?: number;\n /** Membrane push/pull: a smooth dome at the cursor that swells toward you (repel, +) or dents away\n * (attract, −), carried by the sprung field so it drags like a poke under fabric. World units;\n * 0 = off. */\n push?: number;\n /** Drag-wake: while the cursor moves, the surface just BEHIND it is pulled into a trailing trough\n * that heals once you stop; scales with pointer speed. World units; 0 = off. */\n wake?: number;\n /** 0..1 — wireframe strands taper to hairlines; solid gains local translucency. */\n thin?: number;\n /** Local hue rotation near the cursor, degrees. */\n hueShift?: number;\n /** Local brightness lift near the cursor, -1..1. */\n lighten?: number;\n /** Pointer-follow smoothing for THIS wave's hover field, seconds — how quickly the swell trails\n * the cursor. Vary it across a stack so strands lag at different rates (a parallax drag).\n * Default 0.12. */\n smoothing?: number;\n}\n\n/** Click / touch pointer-field: what a tap or click on this wave triggers. */\nexport interface WavePressConfig {\n /** Click-ripple amplitude; 0 keeps this wave's POINTER_RIPPLES path uncompiled. */\n ripple?: number;\n}\n\n/** Per-wave interactivity: this wave's own reaction to the shared pointer + inputs. ABSENT ⇒ inert. */\nexport interface WaveInteractionConfig {\n /** Hover field (cursor-follow agitation / thinning / hue-lighten). */\n hover?: WaveHoverConfig;\n /** Click & touch (ripples radiating from a tap/click on this wave). */\n press?: WavePressConfig;\n /** Input→param bindings driving THIS wave's params (any source, incl. scroll / hover / custom). */\n bindings?: WaveInteractionBinding[];\n}\n\n/** Scene-level interactivity: the SHARED inputs (one cursor + scroll, touch) plus bindings that\n * drive shared scene params. Pointer-follow smoothing is per-wave (see WaveHoverConfig.smoothing).\n * ABSENT ⇒ inputs use defaults; `enabled: false` is the master OFF switch for the whole layer. */\nexport interface SceneInteractionConfig {\n /** Master switch for the whole interaction layer. Default true (only `false` turns it all off). */\n enabled?: boolean;\n /** Pointer falloff radius, as a fraction of viewport height. Default 0.3. */\n radius?: number;\n /** Ribbon flow (0..1, default 0.8): stretch the pointer footprint along each wave's own length axis\n * so the influence reaches ALONG the ribbon instead of as a circular screen disc. On by default;\n * set 0 for the plain circle. Scene-level (shared like `radius`); each wave uses its own length\n * tangent. Only affects waves that already react to the cursor (non-interactive waves are inert). */\n ribbonFlow?: number;\n /** Follow coarse (touch) pointers. Default false — touch is ignored unless this is true. */\n touch?: boolean;\n /** Input→param bindings driving SCENE params (timeOffset, cameraZoom, blur, grain). */\n bindings?: SceneInteractionBinding[];\n}\n\n/**\n * Scene-level settings shared by every wave: output/background/camera/lights, the post-fx\n * pass (grain/blur), playback, quality, and the whole-composition mirror. Everything that\n * describes an individual wave lives on WaveConfig instead.\n */\nexport interface SceneConfig {\n background: string;\n transparentBackground: boolean;\n backgroundMode: BackgroundMode;\n backgroundPalette: ColorStop[];\n backgroundGradientType: GradientType;\n backgroundGradientAngle: number;\n backgroundGradientSource: PaletteSource;\n backgroundMeshPoints: MeshGradientPoint[];\n backgroundMeshSoftness: number;\n backgroundImageSource: PaletteSource;\n backgroundImageUrl?: string;\n backgroundVideoUrl?: string;\n backgroundImageFit: BackgroundImageFit;\n backgroundImageZoom: number;\n backgroundImagePosition: Vec2;\n /** Number of stacked waves (kept in sync with waves.length). */\n waveCount: number;\n quality: number;\n dprMax: number;\n paused: boolean;\n /** Noise phase offset — scrubs the noise pattern to pick a still frame. */\n timeOffset?: number;\n /** Seamless-loop period in seconds (0 = off). When >0, the motion is mapped onto a circle in\n * noise space so it repeats exactly every `loopSeconds` — scene-level so a multi-wave stack\n * shares one period and the whole composite loops. */\n loopSeconds?: number;\n introRamp?: boolean;\n showCameraRig: boolean;\n cameraDistance: number;\n cameraZoom: number;\n cameraPosition: Vec3;\n cameraTarget: Vec3;\n /** How the authored reference frame maps onto the canvas when their aspects differ.\n * Default `\"cover\"`. See {@link CameraFit}. */\n cameraFit?: CameraFit;\n /** Floor on how much of the authored frame's WIDTH stays on screen, as a fraction (0..1).\n * A ceiling on zoom applied AFTER {@link cameraFit}, so the two compose instead of fighting:\n * it only ever zooms out, and is inert for fits that already do (`contain`, `width`).\n *\n * This is the narrow-screen crop control. `cameraFit: \"cover\"` binds on height once the canvas\n * is narrower than the 16:9 reference, so a portrait phone (390×844 @ dpr 2) zooms 2.25× and\n * shows only ~26% of the authored width. `0.6` holds 60% of it on screen; `1` is equivalent to\n * `\"width\"`. Default 0 (off) — existing configs frame exactly as before.\n *\n * It clamps the BASE zoom, before the cameraZoom multiplier, which makes the fraction read\n * against your own composition rather than the raw constant: `1` shows exactly the horizontal\n * span you see at 16:9 whatever cameraZoom you authored at, and `0.6` shows 60% of that. */\n cameraMinVisibleWidth?: number;\n /** Film grain amount (post pass). */\n grain: number;\n /** Soft-focus / spin blur amount (post pass). */\n blur: number;\n blurSamples?: number;\n /** Bloom (post pass, UnrealBloomPass). strength 0 removes the pass entirely, so cost and pixels\n * are identical to bloom-off; radius/threshold only take effect once strength > 0. */\n bloomStrength?: number;\n bloomRadius?: number;\n bloomThreshold?: number;\n /** Ordered (Bayer) dithering over the finished composite — a self-contained \"layered\" post\n * shader in the spirit of paper-design/shaders. 0 removes the pass entirely (cost/pixels match\n * dither-off); scale & steps only bite once dither > 0. Runs last, after tone-map + sRGB. */\n dither?: number;\n /** Dither cell size in device pixels (>=1) — larger = chunkier pattern. */\n ditherScale?: number;\n /** Quantization levels per channel (>=2) — lower = heavier posterization. */\n ditherSteps?: number;\n /** Volumetric light streaks (innerLight) scattered from the bright wave toward a light point\n * (innerLightX/Y in UV). 0 removes the pass; density/decay/centre only bite once innerLight > 0.\n * Scene-zone (scatters the raw wave, like bloom). */\n innerLight?: number;\n innerLightDensity?: number;\n innerLightDecay?: number;\n innerLightX?: number;\n innerLightY?: number;\n /** Halftone: a rotated dot screen (dot size scales with local brightness) over the final image.\n * 0 removes the pass; cell/angle only bite once halftone > 0. Finish-zone stylization. */\n halftone?: number;\n halftoneCell?: number;\n halftoneAngle?: number;\n /** Heatmap recolour (luminance → thermal palette). 0 removes the pass. Finish-zone. */\n heatmap?: number;\n /** Paper-texture overlay (fibrous substrate shading). 0 removes the pass; scale = grain size. */\n paperTexture?: number;\n paperTextureScale?: number;\n /** CMYK halftone (four rotated dot screens). 0 removes the pass; cell = dot size px. */\n halftoneCmyk?: number;\n halftoneCmykCell?: number;\n /** Base ambient light level (0–1). */\n ambient: number;\n lights: LightConfig[];\n /** Mirror the whole composition on screen (world-space flip). */\n mirrorH: boolean;\n mirrorV: boolean;\n /** Shared interaction inputs (one cursor + scroll) + scene-param bindings. Per-wave response\n * lives on each WaveConfig.interaction. ABSENT = defaults; `enabled:false` disables the layer. */\n interaction?: SceneInteractionConfig;\n}\n\n/** The full save-state: scene settings + one or more complete waves. */\nexport interface StudioConfig extends SceneConfig {\n waves: WaveConfig[];\n}\n\n/** Spread a base wave into `count` overlapping waves — each with a slightly varied hue, width,\n * speed, phase, vertical offset and roll so a stack reads as one composition. `count === 1`\n * returns the base unchanged. Used to author multi-wave presets. */\nexport function makeWaveSpread(base: WaveConfig, count: number): WaveConfig[] {\n if (count <= 1) return [structuredClone(base)];\n const out: WaveConfig[] = [];\n for (let i = 0; i < count; i++) {\n const f = i / (count - 1);\n const w = structuredClone(base);\n w.opacity = 1.0 - f * 0.3;\n w.hueShift = base.hueShift + i * 18;\n w.scale = { x: base.scale.x, y: base.scale.y * (1 - f * 0.2), z: base.scale.z };\n w.speed = base.speed * (1 + f * 0.15);\n w.seed = i * 3.3;\n w.position = {\n x: base.position.x,\n y: base.position.y + (f - 0.5) * 1.5,\n z: base.position.z - i * 0.8,\n };\n w.rotation = { x: base.rotation.x, y: base.rotation.y, z: base.rotation.z + i * 20 };\n out.push(w);\n }\n return out;\n}\n\n/** The hero wave (a single complete wave) — the base for the default config and most presets. */\nfunction defaultWave(): WaveConfig {\n return {\n // The hero palette: a periwinkle tip/edge, a dominant orange core, then coral → magenta →\n // pink, with a violet twist tip. gradientShift warps it to mimic a baked 2D palette texture.\n palette: [\n { color: \"#8e9dff\", pos: 0 }, // periwinkle (blue tip/edge)\n { color: \"#c98fd0\", pos: 0.14 }, // lavender transition\n { color: \"#ff9326\", pos: 0.3 }, // orange (rising)\n { color: \"#fd8108\", pos: 0.52 }, // orange core\n { color: \"#fb7a36\", pos: 0.64 }, // orange-coral (keeps orange dominant)\n { color: \"#d24ecc\", pos: 0.78 }, // true magenta (hue ~303, not pink)\n { color: \"#e95cae\", pos: 0.9 }, // pink-magenta\n { color: \"#9b6ae0\", pos: 1.0 }, // violet (twist tip)\n ],\n gradientType: \"linear\",\n gradientAngle: 90, // 90° = the gradient runs ALONG the length (uv.x)\n gradientShift: 0.15,\n meshGradientPoints: createDefaultMeshPoints(),\n meshGradientSoftness: 0.62,\n usePaletteTexture: true, // default to the baked hero LUT\n paletteSource: \"hero\",\n paletteTextureScale: { x: 1, y: 1 },\n paletteTextureOffset: { x: 0, y: 0 },\n paletteTextureRotation: 0,\n paletteDriftX: 0,\n paletteDriftY: 0,\n paletteEdgeColor: \"#8e9dff\",\n paletteEdgeAmount: 0.3,\n hueShift: -1.81, // hero colorHueShift ≈ -1.81°\n colorContrast: 1.0,\n colorSaturation: 1.15,\n // Hero fibers: the surfaceColor fragment hardcodes freq 600 / strength 0.2; the line* fields\n // feed the wireframe theme (unused by the solid hero).\n fiberCount: 600,\n fiberStrength: 0.2,\n noiseBands: [],\n texture: 0,\n creaseLight: 0.6,\n creaseSharpness: 0.589,\n creaseSoftness: 1.0,\n // sheen 0 + roundness 0: the ortho crop makes crease low, so the hero look comes from the\n // SrcColor² blend + the palette, not the derivative white-lift.\n sheen: 0.0,\n roundness: 0.0,\n iridescence: 0,\n edgeFade: 0.04,\n edgeFeather: 0.1, // the original hardcoded ribbon-edge softness\n depthTint: 0,\n depthTintColor: \"#0a2540\",\n // Hero deformation on the native 400-unit folded() geometry.\n displaceFrequency: { x: 0.003234, y: 0.00799 },\n displaceAmount: 6.051,\n detailFrequency: 0.04, // finer than the base swell; only bites once detailAmount > 0\n detailAmount: 0,\n // Small twist frequencies + high powers — a gentle twist; the drama is the ortho crop.\n twistFrequency: { x: -0.055, y: 0.077, z: -0.518 },\n twistPower: { x: 3.95, y: 5.85, z: 6.33 },\n twistMotion: false,\n // Helix off: radius and roll both 0 leave the whole block uncompiled (see waveDefines).\n helixTurns: 0,\n helixRadius: 0,\n helixRoll: 0,\n helixPhase: 0,\n theme: \"solid\",\n lineAmount: 425, // wireframe-theme line params (defaults)\n lineThickness: 1,\n lineDerivativePower: 0.95,\n rungAmount: 0, // cross-wise rungs off\n rungThickness: 1,\n maxWidth: 1232,\n // Hero mesh transform at FULL scale (the ortho camera frames in pixels).\n position: { x: -24.3, y: -56.4, z: -11.1 },\n rotation: { x: -9.14, y: -16.25, z: -161.32 },\n scale: { x: 10, y: 10, z: 7 },\n blendMode: \"squared\", // the hero squaring blend (SrcColor²)\n speed: 0.04, // hero speed: 4e-5 vs ms-time ≈ 0.04/s\n opacity: 1,\n seed: 0,\n };\n}\n\n/** A fresh default wave (the hero wave as one complete wave). */\nexport function makeWave(): WaveConfig {\n return defaultWave();\n}\n\n/** Resize `waves` to match `waveCount`. New waves CLONE the last one (inherit every\n * property of the preceding wave); extras are dropped. */\nexport function resizeWaves(config: StudioConfig): void {\n const target = Math.max(1, Math.round(config.waveCount) || 1);\n if (!Array.isArray(config.waves) || config.waves.length === 0) {\n config.waves = [makeWave()];\n }\n while (config.waves.length < target) {\n config.waves.push(structuredClone(config.waves[config.waves.length - 1]));\n }\n while (config.waves.length > target) config.waves.pop();\n config.waveCount = config.waves.length;\n}\n\n/** The default studio config: the hero wave + its scene, in the canonical wave model. */\nexport function createDefaultConfig(): StudioConfig {\n return {\n background: \"#ffffff\",\n transparentBackground: true,\n backgroundMode: \"color\",\n backgroundPalette: makeStops([\"#0a2540\", \"#425466\", \"#7a73ff\", \"#f6f9fc\"]),\n backgroundGradientType: \"linear\",\n backgroundGradientAngle: 135,\n backgroundGradientSource: \"stops\",\n backgroundMeshPoints: createDefaultMeshPoints(),\n backgroundMeshSoftness: 0.62,\n backgroundImageSource: \"vaporwave\",\n backgroundImageFit: \"cover\",\n backgroundImageZoom: 1,\n backgroundImagePosition: { x: 0, y: 0 },\n waveCount: 1,\n quality: 1,\n dprMax: 2,\n paused: false,\n timeOffset: 0, // noise phase (scrub to pick a still)\n introRamp: true, // ease the animation in over ~1s on load (skipped in dev; see WaveRenderer.updateTime)\n showCameraRig: false,\n // The hero camera: ORTHOGRAPHIC at (100,0,5000) looking at the origin. The mesh is ×10 so\n // the wave overflows the frame and only the twist shows. cameraZoom is a user multiplier on\n // the responsive base zoom (1 = the hero crop); cameraTarget pans the look-at to the twist.\n cameraDistance: 5001,\n cameraPosition: { x: 100, y: 0, z: 5000 },\n cameraTarget: { x: -44, y: -250, z: 0 },\n cameraZoom: 1.0,\n cameraFit: \"cover\",\n cameraMinVisibleWidth: 0, // off — see the field docs for the narrow-screen crop control\n // Post (one pass over the whole composite): hero grain 1.1, blur 0.02.\n grain: 1.1,\n blur: 0.02,\n blurSamples: 6,\n dither: 0, // off by default — the hero look is unchanged (the pass isn't inserted)\n ditherScale: 2,\n ditherSteps: 4,\n innerLight: 0,\n innerLightDensity: 0.5,\n innerLightDecay: 0.95,\n innerLightX: 0.5,\n innerLightY: 0.15,\n halftone: 0,\n halftoneCell: 6,\n halftoneAngle: 0.4,\n heatmap: 0,\n paperTexture: 0,\n paperTextureScale: 2,\n halftoneCmyk: 0,\n halftoneCmykCell: 6,\n ambient: 0.45,\n lights: [], // hero has no lights — colour is the palette + the SrcColor² blend\n mirrorH: false,\n mirrorV: false,\n waves: [defaultWave()],\n };\n}\n\n/** Clamp/backfill a single wave's colour + palette fields (legacy `string[]` palettes become\n * ColorStop[]; mesh points + texture transform are clamped). */\nfunction normalizeWaveColour(config: WaveConfig): void {\n const p = config.palette as unknown as Array<string | ColorStop> | undefined;\n if (!Array.isArray(p) || p.length === 0) {\n // A wave with no usable palette at all (`\"waves\": [{}]`) used to throw right here, out of the\n // very normalizer whose job is to make an untrusted config safe to render.\n config.palette = defaultWave().palette;\n } else if (typeof p[0] === \"string\") {\n config.palette = makeStops(p as string[]);\n }\n if (\n config.gradientType !== \"radial\" &&\n config.gradientType !== \"conic\" &&\n config.gradientType !== \"mesh\" &&\n config.gradientType !== \"linear\"\n ) {\n config.gradientType = \"linear\";\n }\n const rawMeshPoints = config.meshGradientPoints as MeshGradientPoint[] | undefined;\n if (!Array.isArray(rawMeshPoints) || rawMeshPoints.length < 2) {\n config.meshGradientPoints = createDefaultMeshPoints();\n } else {\n const defaults = createDefaultMeshPoints();\n config.meshGradientPoints = rawMeshPoints.slice(0, MAX_MESH_POINTS).map((point, index) => {\n const fallback = defaults[index] ?? defaults[defaults.length - 1];\n const x = Number(point.x);\n const y = Number(point.y);\n const influence = Number(point.influence);\n return {\n color: typeof point.color === \"string\" ? point.color : fallback.color,\n x: clamp01(Number.isFinite(x) ? x : fallback.x),\n y: clamp01(Number.isFinite(y) ? y : fallback.y),\n influence: clamp(Number.isFinite(influence) ? influence : fallback.influence, 0.15, 1.5),\n };\n });\n }\n if (!Number.isFinite(config.meshGradientSoftness)) config.meshGradientSoftness = 0.62;\n config.meshGradientSoftness = clamp01(config.meshGradientSoftness);\n if (!config.paletteTextureScale) config.paletteTextureScale = { x: 1, y: 1 };\n if (!config.paletteTextureOffset) config.paletteTextureOffset = { x: 0, y: 0 };\n config.paletteTextureScale.x = clamp(Number(config.paletteTextureScale.x) || 1, 0.1, 8);\n config.paletteTextureScale.y = clamp(Number(config.paletteTextureScale.y) || 1, 0.1, 8);\n config.paletteTextureOffset.x = clamp(Number(config.paletteTextureOffset.x) || 0, -4, 4);\n config.paletteTextureOffset.y = clamp(Number(config.paletteTextureOffset.y) || 0, -4, 4);\n config.paletteTextureRotation = clamp(Number(config.paletteTextureRotation) || 0, -180, 180);\n}\n\n/** Backfill background styling for states saved before gradient/image backgrounds existed. */\nexport function normalizeBackground(config: StudioConfig): void {\n if (typeof config.background !== \"string\") config.background = \"#ffffff\";\n if (typeof config.transparentBackground !== \"boolean\") config.transparentBackground = true;\n if (\n config.backgroundMode !== \"gradient\" &&\n config.backgroundMode !== \"image\" &&\n config.backgroundMode !== \"color\"\n ) {\n config.backgroundMode = \"color\";\n }\n const palette = config.backgroundPalette as unknown as Array<string | ColorStop> | undefined;\n if (!palette || palette.length < 2) {\n config.backgroundPalette = makeStops([\"#0a2540\", \"#425466\", \"#7a73ff\", \"#f6f9fc\"]);\n } else if (typeof palette[0] === \"string\") {\n config.backgroundPalette = makeStops(palette as string[]);\n }\n if (\n config.backgroundGradientType !== \"radial\" &&\n config.backgroundGradientType !== \"conic\" &&\n config.backgroundGradientType !== \"mesh\" &&\n config.backgroundGradientType !== \"linear\"\n ) {\n config.backgroundGradientType = \"linear\";\n }\n const bgMesh = config.backgroundMeshPoints as MeshGradientPoint[] | undefined;\n if (!Array.isArray(bgMesh) || bgMesh.length < 2) {\n config.backgroundMeshPoints = createDefaultMeshPoints();\n }\n if (!Number.isFinite(config.backgroundMeshSoftness)) config.backgroundMeshSoftness = 0.62;\n config.backgroundMeshSoftness = clamp01(config.backgroundMeshSoftness);\n if (!Number.isFinite(config.backgroundGradientAngle)) config.backgroundGradientAngle = 135;\n if (typeof config.backgroundGradientSource !== \"string\")\n config.backgroundGradientSource = \"stops\";\n if (typeof config.backgroundImageSource !== \"string\") config.backgroundImageSource = \"vaporwave\";\n if (\n config.backgroundImageFit !== \"contain\" &&\n config.backgroundImageFit !== \"stretch\" &&\n config.backgroundImageFit !== \"cover\"\n ) {\n config.backgroundImageFit = \"cover\";\n }\n if (!Number.isFinite(config.backgroundImageZoom)) config.backgroundImageZoom = 1;\n config.backgroundImageZoom = clamp(config.backgroundImageZoom, 0.1, 8);\n if (!config.backgroundImagePosition) config.backgroundImagePosition = { x: 0, y: 0 };\n if (typeof config.backgroundImagePosition.x !== \"number\") config.backgroundImagePosition.x = 0;\n if (typeof config.backgroundImagePosition.y !== \"number\") config.backgroundImagePosition.y = 0;\n config.backgroundImagePosition.x = clamp(config.backgroundImagePosition.x, -100, 100);\n config.backgroundImagePosition.y = clamp(config.backgroundImagePosition.y, -100, 100);\n}\n\n/** Backfill camera position/target for states saved before they existed. */\nexport function ensureCamera(config: StudioConfig): void {\n if (!config.cameraPosition)\n config.cameraPosition = { x: 0, y: 0, z: config.cameraDistance ?? 62 };\n if (!config.cameraTarget) config.cameraTarget = { x: 0, y: 0, z: 0 };\n if (!Number.isFinite(config.cameraZoom)) config.cameraZoom = 1;\n // Framing policy: absent → the historical cover framing, so every saved config/preset that\n // predates these fields reproduces byte-identically.\n if (!CAMERA_FITS.includes(config.cameraFit as CameraFit)) config.cameraFit = \"cover\";\n config.cameraMinVisibleWidth =\n typeof config.cameraMinVisibleWidth === \"number\"\n ? clamp(config.cameraMinVisibleWidth, 0, 1)\n : 0;\n}\n\n/** Backfill/repair a wave so the renderer can consume it (covers partial wave-model JSON). */\nexport function normalizeWave(s: WaveConfig): void {\n normalizeWaveColour(s);\n if (!Number.isFinite(s.gradientAngle)) s.gradientAngle = 90;\n if (!Number.isFinite(s.gradientShift)) s.gradientShift = 0.15;\n if (typeof s.usePaletteTexture !== \"boolean\") s.usePaletteTexture = true;\n if (typeof s.paletteSource !== \"string\") s.paletteSource = \"hero\";\n if (typeof s.paletteEdgeColor !== \"string\") s.paletteEdgeColor = \"#8e9dff\";\n if (!Number.isFinite(s.paletteEdgeAmount)) s.paletteEdgeAmount = 0.3;\n if (!Number.isFinite(s.paletteDriftX)) s.paletteDriftX = 0;\n if (!Number.isFinite(s.paletteDriftY)) s.paletteDriftY = 0;\n if (!Number.isFinite(s.hueShift)) s.hueShift = 0;\n if (!Number.isFinite(s.colorContrast)) s.colorContrast = 1;\n if (!Number.isFinite(s.colorSaturation)) s.colorSaturation = 1;\n if (!Number.isFinite(s.fiberCount)) s.fiberCount = 600;\n if (!Number.isFinite(s.fiberStrength)) s.fiberStrength = 0.2;\n if (!Array.isArray(s.noiseBands)) s.noiseBands = [];\n normalizeNoiseBands(s);\n if (!Number.isFinite(s.texture)) s.texture = 0;\n if (!Number.isFinite(s.creaseLight)) s.creaseLight = 0.6;\n if (!Number.isFinite(s.creaseSharpness)) s.creaseSharpness = 0.589;\n if (!Number.isFinite(s.creaseSoftness)) s.creaseSoftness = 1;\n if (!Number.isFinite(s.sheen)) s.sheen = 0;\n if (!Number.isFinite(s.roundness)) s.roundness = 0;\n if (!Number.isFinite(s.iridescence)) s.iridescence = 0;\n if (!Number.isFinite(s.edgeFade)) s.edgeFade = 0.04;\n if (!Number.isFinite(s.edgeFeather)) s.edgeFeather = 0.1;\n if (!Number.isFinite(s.depthTint)) s.depthTint = 0;\n if (typeof s.depthTintColor !== \"string\") s.depthTintColor = \"#0a2540\";\n if (!s.displaceFrequency) s.displaceFrequency = { x: 0.003234, y: 0.00799 };\n if (!Number.isFinite(s.displaceAmount)) s.displaceAmount = 6.051;\n if (!Number.isFinite(s.detailFrequency)) s.detailFrequency = 0.04;\n if (!Number.isFinite(s.detailAmount)) s.detailAmount = 0;\n if (!s.twistFrequency) s.twistFrequency = { x: -0.055, y: 0.077, z: -0.518 };\n if (!s.twistPower) s.twistPower = { x: 3.95, y: 5.85, z: 6.33 };\n // false, not absent: the panel binds this directly, and a wave that omits it can't be edited.\n if (typeof s.twistMotion !== \"boolean\") s.twistMotion = false;\n if (!Number.isFinite(s.helixTurns)) s.helixTurns = 0;\n if (!Number.isFinite(s.helixRadius)) s.helixRadius = 0;\n if (!Number.isFinite(s.helixRoll)) s.helixRoll = 0;\n if (!Number.isFinite(s.helixPhase)) s.helixPhase = 0;\n if (typeof s.theme !== \"string\") s.theme = \"solid\";\n if (!Number.isFinite(s.lineAmount)) s.lineAmount = 425;\n if (!Number.isFinite(s.lineThickness)) s.lineThickness = 1;\n if (!Number.isFinite(s.lineDerivativePower)) s.lineDerivativePower = 0.95;\n if (!Number.isFinite(s.rungAmount)) s.rungAmount = 0;\n if (!Number.isFinite(s.rungThickness)) s.rungThickness = 1;\n if (!Number.isFinite(s.maxWidth)) s.maxWidth = 1232;\n if (!s.position) s.position = { x: 0, y: 0, z: 0 };\n if (!s.rotation) s.rotation = { x: 0, y: 0, z: 0 };\n if (!s.scale) s.scale = { x: 10, y: 10, z: 7 };\n if (typeof s.blendMode !== \"string\") s.blendMode = \"squared\";\n if (!Number.isFinite(s.speed)) s.speed = 0.04;\n if (!Number.isFinite(s.opacity)) s.opacity = 1;\n if (!Number.isFinite(s.seed)) s.seed = 0;\n if (s.interaction) normalizeWaveInteraction(s); // present-only; absence stays inert\n}\n\n/** Backfill scene-level defaults (background/camera/post/lights/quality/mirror). */\nexport function ensureSceneDefaults(config: StudioConfig): void {\n normalizeBackground(config);\n ensureCamera(config);\n if (!Number.isFinite(config.ambient)) config.ambient = 0.45;\n if (!Array.isArray(config.lights)) config.lights = [];\n normalizeLights(config);\n if (!Number.isFinite(config.quality)) config.quality = 1;\n if (!Number.isFinite(config.dprMax)) config.dprMax = 2;\n if (!Number.isFinite(config.grain)) config.grain = 1.1;\n if (!Number.isFinite(config.blur)) config.blur = 0.02;\n if (!Number.isFinite(config.blurSamples)) config.blurSamples = 6;\n if (!Number.isFinite(config.bloomStrength)) config.bloomStrength = 0;\n if (!Number.isFinite(config.bloomRadius)) config.bloomRadius = 0.4;\n if (!Number.isFinite(config.bloomThreshold)) config.bloomThreshold = 0.85;\n if (!Number.isFinite(config.dither)) config.dither = 0;\n if (!Number.isFinite(config.ditherScale)) config.ditherScale = 2;\n if (!Number.isFinite(config.ditherSteps)) config.ditherSteps = 4;\n if (!Number.isFinite(config.innerLight)) config.innerLight = 0;\n if (!Number.isFinite(config.innerLightDensity)) config.innerLightDensity = 0.5;\n if (!Number.isFinite(config.innerLightDecay)) config.innerLightDecay = 0.95;\n if (!Number.isFinite(config.innerLightX)) config.innerLightX = 0.5;\n if (!Number.isFinite(config.innerLightY)) config.innerLightY = 0.15;\n if (!Number.isFinite(config.halftone)) config.halftone = 0;\n if (!Number.isFinite(config.halftoneCell)) config.halftoneCell = 6;\n if (!Number.isFinite(config.halftoneAngle)) config.halftoneAngle = 0.4;\n if (!Number.isFinite(config.heatmap)) config.heatmap = 0;\n if (!Number.isFinite(config.paperTexture)) config.paperTexture = 0;\n if (!Number.isFinite(config.paperTextureScale)) config.paperTextureScale = 2;\n if (!Number.isFinite(config.halftoneCmyk)) config.halftoneCmyk = 0;\n if (!Number.isFinite(config.halftoneCmykCell)) config.halftoneCmykCell = 6;\n if (typeof config.showCameraRig !== \"boolean\") config.showCameraRig = false;\n if (typeof config.paused !== \"boolean\") config.paused = false;\n // Not clamped to the studio slider's 0..60: a driver stepping a paused scene frame by frame\n // (the embed's timeOffset drive) legitimately passes any finite phase.\n if (!Number.isFinite(config.timeOffset)) config.timeOffset = 0;\n if (!Number.isFinite(config.loopSeconds)) config.loopSeconds = 0;\n if (typeof config.mirrorH !== \"boolean\") config.mirrorH = false;\n if (typeof config.mirrorV !== \"boolean\") config.mirrorV = false;\n // NOTE: `interaction` (scene + per-wave) is deliberately NOT backfilled — absence is semantically\n // \"off\" and keeps the compiled shader byte-identical. The present-only normalizers below run from\n // ensureStudioConfig / normalizeWave only when a block is actually present.\n}\n\n/** Clamp an untrusted numeric field, falling back to `dflt` when it isn't a finite number. */\nfunction clampNumber(v: unknown, min: number, max: number, dflt: number): number {\n const n = Number(v);\n return Number.isFinite(n) ? clamp(n, min, max) : dflt;\n}\n\n/** Coerce an untrusted field to a finite number, falling back to `dflt`. Deliberately does NOT\n * clamp — it repairs broken values without reinterpreting out-of-range ones that a saved config\n * may legitimately rely on. */\nfunction num(v: unknown, dflt: number): number {\n const n = Number(v);\n return Number.isFinite(n) ? n : dflt;\n}\n\n/**\n * Repair the ELEMENTS of the scene's `lights` and a wave's `noiseBands`. Both arrays are\n * type-checked where they're backfilled, but their entries never were — and the studio binds every\n * field below directly, so one hand-authored `\"lights\": [{}]` was enough to make the whole control\n * panel unbuildable. Entries that aren't objects at all are dropped.\n */\nfunction normalizeLights(config: StudioConfig): void {\n config.lights = config.lights.filter((l) => typeof l === \"object\" && l !== null);\n for (const l of config.lights) {\n if (typeof l.position !== \"object\" || l.position === null) {\n l.position = { ...DEFAULT_LIGHT_POSITION };\n }\n l.position.x = num(l.position.x, DEFAULT_LIGHT_POSITION.x);\n l.position.y = num(l.position.y, DEFAULT_LIGHT_POSITION.y);\n l.position.z = num(l.position.z, DEFAULT_LIGHT_POSITION.z);\n if (typeof l.color !== \"string\") l.color = \"#ffffff\";\n l.intensity = num(l.intensity, 1);\n }\n}\n\nfunction normalizeNoiseBands(s: WaveConfig): void {\n s.noiseBands = s.noiseBands.filter((b) => typeof b === \"object\" && b !== null);\n const d = createNoiseBand();\n for (const b of s.noiseBands) {\n b.startX = num(b.startX, d.startX);\n b.endX = num(b.endX, d.endX);\n b.startY = num(b.startY, d.startY);\n b.endY = num(b.endY, d.endY);\n b.feather = num(b.feather, d.feather);\n b.strength = num(b.strength, d.strength);\n b.frequency = num(b.frequency, d.frequency);\n b.colorAttenuation = num(b.colorAttenuation, d.colorAttenuation);\n b.parabolaPower = num(b.parabolaPower, d.parabolaPower);\n }\n}\n\n/** True for a valid interaction source string: a built-in name or a non-empty `custom:<name>`. */\nfunction isInteractionSource(v: unknown): v is InteractionSource {\n return (\n typeof v === \"string\" &&\n ((INTERACTION_SOURCE_NAMES as readonly string[]).includes(v) ||\n (v.startsWith(\"custom:\") && v.length > \"custom:\".length))\n );\n}\n\n/** Rebuild an untrusted bindings array into valid bindings for `valid` targets (loaded share-links /\n * presets are untrusted JSON; we validate source/target/to and rebuild clean objects). */\nfunction cleanBindings<T extends string>(\n raw: unknown,\n valid: readonly string[],\n): Array<InteractionBindingBase & { target: T }> {\n const out: Array<InteractionBindingBase & { target: T }> = [];\n if (!Array.isArray(raw)) return out;\n for (const item of raw) {\n if (!item || typeof item !== \"object\") continue;\n const b = item as Record<string, unknown>;\n if (!isInteractionSource(b.source)) continue;\n if (!valid.includes(b.target as string)) continue;\n const to = Number(b.to);\n if (!Number.isFinite(to)) continue;\n const clean: InteractionBindingBase & { target: T } = {\n source: b.source,\n target: b.target as T,\n to,\n };\n if (b.from !== undefined) {\n const f = Number(b.from);\n if (Number.isFinite(f)) clean.from = f;\n }\n if (b.smoothing !== undefined) clean.smoothing = clampNumber(b.smoothing, 0, 2, 0.25);\n out.push(clean);\n }\n return out;\n}\n\n/**\n * Present-only normalizer for a WAVE's interaction block: clamp the hover/press numerics that are\n * present (absent fields stay absent, so the block stays lean and the renderer's defaults apply) and\n * drop bindings with an unknown source/target or a non-finite `to`. NEVER call when the block is\n * absent — absence is inert and byte-identical (normalizeWave gates on presence).\n */\nexport function normalizeWaveInteraction(wave: WaveConfig): void {\n const it = wave.interaction;\n if (!it) return;\n const h = it.hover;\n if (h) {\n if (h.agitate !== undefined) h.agitate = clampNumber(h.agitate, 0, 60, 0);\n if (h.push !== undefined) h.push = clampNumber(h.push, -40, 40, 0);\n if (h.wake !== undefined) h.wake = clampNumber(h.wake, 0, 40, 0);\n if (h.thin !== undefined) h.thin = clampNumber(h.thin, 0, 1, 0);\n if (h.hueShift !== undefined) h.hueShift = clampNumber(h.hueShift, -360, 360, 0);\n if (h.lighten !== undefined) h.lighten = clampNumber(h.lighten, -1, 1, 0);\n if (h.smoothing !== undefined) h.smoothing = clampNumber(h.smoothing, 0, 2, 0.12);\n }\n if (it.press && it.press.ripple !== undefined) {\n it.press.ripple = clampNumber(it.press.ripple, 0, 60, 0);\n }\n if (it.bindings !== undefined) {\n it.bindings = cleanBindings<WaveInteractionTarget>(it.bindings, WAVE_TARGET_NAMES);\n }\n}\n\n/** Present-only normalizer for the SCENE interaction block: clamp the shared pointer inputs and drop\n * invalid scene bindings. NEVER call when the block is absent. */\nexport function normalizeSceneInteraction(config: StudioConfig): void {\n const it = config.interaction;\n if (!it) return;\n if (it.radius !== undefined) it.radius = clampNumber(it.radius, 0.02, 2, 0.3);\n if (it.ribbonFlow !== undefined) it.ribbonFlow = clampNumber(it.ribbonFlow, 0, 1, 0.8);\n if (it.bindings !== undefined) {\n it.bindings = cleanBindings<SceneInteractionTarget>(it.bindings, SCENE_TARGET_NAMES);\n }\n}\n\n/** Normalize an ingested config to the wave model: backfill the scene + every wave, and drop in\n * a default wave if none are present. Idempotent, so it is safe on the renderer's own config as\n * well as freshly loaded save-states / share links. */\nexport function ensureStudioConfig(input: StudioConfig): StudioConfig {\n const config = input;\n ensureSceneDefaults(config);\n if (!Array.isArray(config.waves) || config.waves.length === 0) {\n config.waves = [makeWave()];\n }\n config.waves.forEach(normalizeWave); // each wave's normalizeWave runs normalizeWaveInteraction\n config.waveCount = config.waves.length;\n // Present-only: a config without a scene `interaction` block is left untouched (stays \"off\").\n if (config.interaction) normalizeSceneInteraction(config);\n return config;\n}\n"],"mappings":"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"}
@@ -1,7 +1,5 @@
1
1
  import { createDefaultConfig } from "./config/model.js";
2
2
  import { WaveRenderer } from "./renderer/WaveRenderer.js";
3
-
4
- //#region src/core-loader.d.ts
5
3
  declare namespace core_loader_d_exports {
6
4
  export { WaveRenderer, createDefaultConfig };
7
5
  }
package/dist/presets.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { StudioConfig } from "./config/model.js";
2
-
3
2
  //#region src/presets.d.ts
4
3
  /** Presets: each a complete studio config (scene + one or more waves) in the wave model. */
5
4
  declare const PRESETS: Record<string, () => StudioConfig>;
package/dist/presets.js CHANGED
@@ -541,6 +541,111 @@ const PRESETS = {
541
541
  c.transparentBackground = false;
542
542
  return c;
543
543
  },
544
+ Corkscrew: () => {
545
+ const c = PRESETS["Hero"]();
546
+ const w = c.waves[0];
547
+ w.helixTurns = 5;
548
+ w.helixRadius = 45;
549
+ w.helixRoll = 1;
550
+ w.helixPhase = 0;
551
+ w.twistFrequency = {
552
+ x: 0,
553
+ y: 0,
554
+ z: 0
555
+ };
556
+ w.twistPower = {
557
+ x: 4,
558
+ y: 4,
559
+ z: 2
560
+ };
561
+ w.displaceAmount = 16;
562
+ w.displaceFrequency = {
563
+ x: .006,
564
+ y: 8e-4
565
+ };
566
+ w.speed = .1;
567
+ w.position = {
568
+ x: 0,
569
+ y: 0,
570
+ z: 0
571
+ };
572
+ w.rotation = {
573
+ x: 0,
574
+ y: 0,
575
+ z: 12
576
+ };
577
+ w.scale = {
578
+ x: 3,
579
+ y: 3,
580
+ z: 1.5
581
+ };
582
+ w.gradientType = "mesh";
583
+ w.meshGradientPoints = [
584
+ {
585
+ color: "#0a84ff",
586
+ x: .06,
587
+ y: .9,
588
+ influence: .68
589
+ },
590
+ {
591
+ color: "#64d2ff",
592
+ x: .88,
593
+ y: .92,
594
+ influence: .72
595
+ },
596
+ {
597
+ color: "#bf5af2",
598
+ x: .5,
599
+ y: .64,
600
+ influence: .58
601
+ },
602
+ {
603
+ color: "#ff375f",
604
+ x: .1,
605
+ y: .14,
606
+ influence: .7
607
+ },
608
+ {
609
+ color: "#ff9f0a",
610
+ x: .84,
611
+ y: .12,
612
+ influence: .74
613
+ },
614
+ {
615
+ color: "#30d158",
616
+ x: .94,
617
+ y: .5,
618
+ influence: .54
619
+ }
620
+ ];
621
+ w.meshGradientSoftness = .68;
622
+ w.blendMode = "normal";
623
+ w.hueShift = 0;
624
+ w.colorContrast = 1.06;
625
+ w.colorSaturation = 1.12;
626
+ w.fiberStrength = .14;
627
+ c.cameraPosition = {
628
+ x: -526.009,
629
+ y: -285.284,
630
+ z: -425.489
631
+ };
632
+ c.cameraTarget = {
633
+ x: -95.046,
634
+ y: -17.053,
635
+ z: -105.608
636
+ };
637
+ c.cameraDistance = 600;
638
+ c.cameraZoom = 1.176;
639
+ c.grain = .3;
640
+ c.blur = .008;
641
+ c.bloomStrength = .35;
642
+ c.bloomRadius = .6;
643
+ c.bloomThreshold = .55;
644
+ c.background = "#070914";
645
+ c.backgroundMode = "color";
646
+ c.transparentBackground = false;
647
+ return c;
648
+ },
544
649
  Kaleidoscope: () => {
545
650
  const c = PRESETS["Wave 3"]();
546
651
  const w = c.waves[0];
@@ -1 +1 @@
1
- {"version":3,"file":"presets.js","names":[],"sources":["../src/presets.ts"],"sourcesContent":["/**\n * Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.\n * IP-clean — no copyrighted assets. The studio layers its own extra presets (and its historical\n * \"Stripe *\" display names) on top; see apps/studio/src/presets.ts.\n */\nimport { createDefaultConfig, makeStops, makeWaveSpread } from \"./config/model\";\nimport type { StudioConfig, NoiseBand } from \"./config/model\";\n\nconst RAD = 180 / Math.PI;\n\n/** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and\n * converted to degrees. All presets are solid-theme, so they reuse the hero palette +\n * surfaceColor fibers (600/0.2) and sheen 0, like the hero. camTarget/zoom frame the\n * wave (we pan the look-at to centre each one). */\nfunction buildPreset(p: {\n speed: number;\n contrast: number;\n sat: number;\n hueRad: number;\n dispX: number;\n dispZ: number;\n dispAmt: number;\n pos: [number, number, number];\n rotRad: [number, number, number];\n scale: [number, number, number];\n twF: [number, number, number];\n twP: [number, number, number];\n glow: [number, number, number];\n grain: number;\n blur: number;\n zoom: number;\n camTarget: [number, number];\n noiseBands?: NoiseBand[];\n twistMotion?: boolean;\n}): StudioConfig {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.speed = p.speed;\n w.colorContrast = p.contrast;\n w.colorSaturation = p.sat;\n w.hueShift = p.hueRad * RAD;\n w.displaceFrequency = { x: p.dispX, y: p.dispZ };\n w.displaceAmount = p.dispAmt;\n w.position = { x: p.pos[0], y: p.pos[1], z: p.pos[2] };\n w.rotation = { x: p.rotRad[0] * RAD, y: p.rotRad[1] * RAD, z: p.rotRad[2] * RAD };\n w.scale = { x: p.scale[0], y: p.scale[1], z: p.scale[2] };\n w.twistFrequency = { x: p.twF[0], y: p.twF[1], z: p.twF[2] };\n w.twistPower = { x: p.twP[0], y: p.twP[1], z: p.twP[2] };\n w.creaseLight = p.glow[0];\n w.creaseSharpness = p.glow[1];\n w.creaseSoftness = p.glow[2];\n if (p.noiseBands) w.noiseBands = p.noiseBands;\n if (p.twistMotion) w.twistMotion = true;\n c.grain = p.grain;\n c.blur = p.blur;\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: p.camTarget[0], y: p.camTarget[1], z: 0 };\n c.cameraZoom = p.zoom;\n return c;\n}\n\n/** Presets: each a complete studio config (scene + one or more waves) in the wave model. */\nexport const PRESETS: Record<string, () => StudioConfig> = {\n // The app's default wave: a centred, full-frame ribbon (window-independent framing).\n // Shown first and named \"Hero\"; several presets below derive from it.\n Hero: () =>\n buildPreset({\n speed: 0.04,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [380, -301.7, -11.1],\n rotRad: [-0.44959, -0.11759, 1.874407],\n scale: [9, 8, 5],\n twF: [-0.65, 0.41, -0.58],\n twP: [3.63, 0.7, 3.95],\n glow: [1.98, 0.806, 0.834],\n grain: 1.1,\n blur: 0.02,\n zoom: 0.55,\n camTarget: [-420, -200], // user-tuned default framing\n }),\n // Stripe's real hero, recreated faithfully: an orthographic ×10 scene that overflows the\n // frame, so only the twisted crop shows. This is the model's plain default config.\n \"Wave 2\": () => createDefaultConfig(),\n // camTarget on the waves below is a first-pass centring; tune per-wave. NOTE: Wave 4 also\n // uses a variant vertex shader (animated twist-X wobble) we don't fully replicate — its\n // STATIC frame is close, the motion differs.\n \"Wave 3\": () =>\n buildPreset({\n speed: 0.08,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [-200.7, -65.4, -11.1],\n rotRad: [-2.875593, 3.095927, -2.925927],\n scale: [3, 3, 3],\n twF: [0.059, 0.32, -0.397],\n twP: [3.63, 0.44, 5.99],\n glow: [3.86, 0.923, 1],\n grain: 1.2,\n blur: 0.02,\n zoom: 1.3,\n camTarget: [-104, 13], // centred; zoomed in (wide/flat wave)\n }),\n \"Wave 4\": () =>\n buildPreset({\n speed: 0.0525,\n contrast: 0.969,\n sat: 1.383,\n hueRad: 0.0376991,\n dispX: 0.005,\n dispZ: 0.0212,\n dispAmt: 6.68,\n pos: [206.1, -438, -11.1],\n rotRad: [-0.666018, -0.031416, 0.779115],\n scale: [6.0501, 8.3983, 6.9854],\n twF: [-0.424, 0.024, -1.312],\n twP: [1.81, 0.94, 4.76],\n glow: [1.55, 1.174, 0.972],\n grain: 0.576,\n blur: 0,\n zoom: 0.9316,\n camTarget: [194, -402], // centred on the wave\n twistMotion: true, // variant vertex shader — animated twist-X wobble\n noiseBands: [\n {\n startX: 0.856,\n endX: 1,\n startY: 0,\n endY: 0.913,\n feather: 0.5,\n strength: 0.346,\n frequency: 1018,\n colorAttenuation: 1,\n parabolaPower: 0,\n },\n {\n startX: 0.038,\n endX: 0.538,\n startY: 0.105,\n endY: 1,\n feather: 0.3315,\n strength: 1,\n frequency: 190,\n colorAttenuation: 0,\n parabolaPower: 2.11,\n },\n ],\n }),\n // The dark-background hero: identical geometry/camera to the default hero, but theme\n // \"wireframe\" → the line shader on a dark page background, with grain 1.2. Same palette.\n Wireframe: () => {\n const c = createDefaultConfig();\n c.waves[0].theme = \"wireframe\";\n c.grain = 1.2;\n c.background = \"#0a2540\"; // dark navy page background\n c.transparentBackground = false;\n return c;\n },\n \"Neon Dark Multistrand\": () => {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.theme = \"wireframe\"; // line shader on the near-black background — neon wireframe look\n w.blendMode = \"additive\";\n w.palette = makeStops([\"#00f5d4\", \"#00bbf9\", \"#9b5de5\", \"#f15bb5\", \"#fee440\"]);\n w.creaseLight = 1.0;\n c.background = \"#05060c\";\n c.transparentBackground = false; // fill the dark bg so the neon lines read on black (not the page)\n c.waves = makeWaveSpread(w, 3); // three overlapping neon waves\n c.waveCount = 3;\n return c;\n },\n \"Mesh Gradient\": () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n c.grain = 0.3;\n c.blur = 0.008;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n \"Solar Bloom\": () => {\n // Radial gradient: a warm core blooming out to a deep-indigo edge. usePaletteTexture off so\n // our own stops map along the radial gradCoord instead of sampling the baked hero LUT.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"radial\";\n w.gradientShift = 0.14;\n w.palette = [\n { color: \"#fff3c4\", pos: 0 }, // warm-white core\n { color: \"#ffd166\", pos: 0.22 }, // gold\n { color: \"#ff8c42\", pos: 0.42 }, // orange\n { color: \"#ff5d8f\", pos: 0.62 }, // coral-pink\n { color: \"#a64dff\", pos: 0.82 }, // violet\n { color: \"#241246\", pos: 1 }, // deep indigo edge\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.01;\n // Deep warm radial vignette behind the bloom.\n c.background = \"#0a0714\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"radial\";\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#2a1330\", \"#08040f\"]);\n c.transparentBackground = false;\n return c;\n },\n Holographic: () => {\n // Conic gradient: an iridescent oil-slick sweep. The palette wraps (first ≈ last stop) so\n // the conic seam is invisible.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"conic\";\n w.gradientShift = 0.08;\n w.palette = [\n { color: \"#8ef6e4\", pos: 0 }, // mint (seam)\n { color: \"#6ec3ff\", pos: 0.18 }, // sky\n { color: \"#9b8cff\", pos: 0.36 }, // periwinkle\n { color: \"#ff8ad8\", pos: 0.54 }, // pink\n { color: \"#ffd98e\", pos: 0.72 }, // peach\n { color: \"#a0f0c8\", pos: 0.88 }, // seafoam\n { color: \"#8ef6e4\", pos: 1 }, // mint again (seamless wrap)\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.04;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.12;\n c.grain = 0.28;\n c.blur = 0.01;\n // Subtle deep teal → violet wash behind the iridescence.\n c.background = \"#05060c\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"linear\";\n c.backgroundGradientAngle = 135;\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#04121a\", \"#0a0518\"]);\n c.transparentBackground = false;\n return c;\n },\n Aurora: () => {\n // Mesh gradient: a moody aurora — teals/greens drifting into violet over a night-sky base\n // (distinct from the brighter iOS-style \"Mesh Gradient\").\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a1f3c\", x: 0.08, y: 0.12, influence: 0.62 },\n { color: \"#1fddb0\", x: 0.3, y: 0.7, influence: 0.78 },\n { color: \"#57f5a3\", x: 0.58, y: 0.86, influence: 0.7 },\n { color: \"#3a86ff\", x: 0.82, y: 0.55, influence: 0.62 },\n { color: \"#a15cff\", x: 0.5, y: 0.32, influence: 0.7 },\n { color: \"#071433\", x: 0.92, y: 0.08, influence: 0.6 },\n ];\n w.meshGradientSoftness = 0.72;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.008;\n // Dark night-sky MESH backdrop (also shows off the mesh background type).\n c.background = \"#03060f\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundMeshPoints = [\n { color: \"#02040c\", x: 0.15, y: 0.85, influence: 0.7 },\n { color: \"#08243a\", x: 0.5, y: 0.5, influence: 0.75 },\n { color: \"#0a0f2e\", x: 0.85, y: 0.7, influence: 0.7 },\n { color: \"#04121a\", x: 0.7, y: 0.2, influence: 0.6 },\n { color: \"#000208\", x: 0.12, y: 0.12, influence: 0.6 },\n ];\n c.backgroundMeshSoftness = 0.75;\n c.transparentBackground = false;\n return c;\n },\n Palestine: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"palestine\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1;\n c.grain = 0.35;\n c.background = \"#f2efe8\";\n c.transparentBackground = true;\n return c;\n },\n Spain: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"spain\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.18;\n w.colorSaturation = 1.25;\n w.creaseLight = 1.6; // moderate crease-light: rich crimson without washing to salmon (Hero's is 1.98)\n c.grain = 0.3;\n c.background = \"#1a0608\"; // deep oxblood stage\n c.backgroundMode = \"color\";\n c.transparentBackground = false; // opaque, so the dark stage makes the flag pop\n return c;\n },\n \"Vaporwave Sunset\": () => {\n // The Hero wave re-posed/re-framed, plus the vaporwave palette.\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.position.x = 525;\n w.rotation.x = -0.64 * RAD;\n w.rotation.z = 1.68 * RAD;\n w.paletteSource = \"vaporwave\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.08;\n w.colorSaturation = 1.15;\n w.creaseLight = 1.25;\n c.cameraZoom = 1.1;\n c.cameraTarget = { x: 150, y: 360, z: 0 };\n c.background = \"#09051f\";\n c.transparentBackground = false;\n return c;\n },\n Kaleidoscope: () => {\n const c = PRESETS[\"Wave 3\"]();\n const w = c.waves[0];\n w.paletteSource = \"kaleidoscope\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.12;\n c.grain = 0.5;\n return c;\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"presets.js","names":[],"sources":["../src/presets.ts"],"sourcesContent":["/**\n * Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.\n * IP-clean — no copyrighted assets. The studio layers its own extra presets (and its historical\n * \"Stripe *\" display names) on top; see apps/studio/src/presets.ts.\n */\nimport { createDefaultConfig, makeStops, makeWaveSpread } from \"./config/model\";\nimport type { StudioConfig, NoiseBand } from \"./config/model\";\n\nconst RAD = 180 / Math.PI;\n\n/** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and\n * converted to degrees. All presets are solid-theme, so they reuse the hero palette +\n * surfaceColor fibers (600/0.2) and sheen 0, like the hero. camTarget/zoom frame the\n * wave (we pan the look-at to centre each one). */\nfunction buildPreset(p: {\n speed: number;\n contrast: number;\n sat: number;\n hueRad: number;\n dispX: number;\n dispZ: number;\n dispAmt: number;\n pos: [number, number, number];\n rotRad: [number, number, number];\n scale: [number, number, number];\n twF: [number, number, number];\n twP: [number, number, number];\n glow: [number, number, number];\n grain: number;\n blur: number;\n zoom: number;\n camTarget: [number, number];\n noiseBands?: NoiseBand[];\n twistMotion?: boolean;\n}): StudioConfig {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.speed = p.speed;\n w.colorContrast = p.contrast;\n w.colorSaturation = p.sat;\n w.hueShift = p.hueRad * RAD;\n w.displaceFrequency = { x: p.dispX, y: p.dispZ };\n w.displaceAmount = p.dispAmt;\n w.position = { x: p.pos[0], y: p.pos[1], z: p.pos[2] };\n w.rotation = { x: p.rotRad[0] * RAD, y: p.rotRad[1] * RAD, z: p.rotRad[2] * RAD };\n w.scale = { x: p.scale[0], y: p.scale[1], z: p.scale[2] };\n w.twistFrequency = { x: p.twF[0], y: p.twF[1], z: p.twF[2] };\n w.twistPower = { x: p.twP[0], y: p.twP[1], z: p.twP[2] };\n w.creaseLight = p.glow[0];\n w.creaseSharpness = p.glow[1];\n w.creaseSoftness = p.glow[2];\n if (p.noiseBands) w.noiseBands = p.noiseBands;\n if (p.twistMotion) w.twistMotion = true;\n c.grain = p.grain;\n c.blur = p.blur;\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: p.camTarget[0], y: p.camTarget[1], z: 0 };\n c.cameraZoom = p.zoom;\n return c;\n}\n\n/** Presets: each a complete studio config (scene + one or more waves) in the wave model. */\nexport const PRESETS: Record<string, () => StudioConfig> = {\n // The app's default wave: a centred, full-frame ribbon (window-independent framing).\n // Shown first and named \"Hero\"; several presets below derive from it.\n Hero: () =>\n buildPreset({\n speed: 0.04,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [380, -301.7, -11.1],\n rotRad: [-0.44959, -0.11759, 1.874407],\n scale: [9, 8, 5],\n twF: [-0.65, 0.41, -0.58],\n twP: [3.63, 0.7, 3.95],\n glow: [1.98, 0.806, 0.834],\n grain: 1.1,\n blur: 0.02,\n zoom: 0.55,\n camTarget: [-420, -200], // user-tuned default framing\n }),\n // Stripe's real hero, recreated faithfully: an orthographic ×10 scene that overflows the\n // frame, so only the twisted crop shows. This is the model's plain default config.\n \"Wave 2\": () => createDefaultConfig(),\n // camTarget on the waves below is a first-pass centring; tune per-wave. NOTE: Wave 4 also\n // uses a variant vertex shader (animated twist-X wobble) we don't fully replicate — its\n // STATIC frame is close, the motion differs.\n \"Wave 3\": () =>\n buildPreset({\n speed: 0.08,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [-200.7, -65.4, -11.1],\n rotRad: [-2.875593, 3.095927, -2.925927],\n scale: [3, 3, 3],\n twF: [0.059, 0.32, -0.397],\n twP: [3.63, 0.44, 5.99],\n glow: [3.86, 0.923, 1],\n grain: 1.2,\n blur: 0.02,\n zoom: 1.3,\n camTarget: [-104, 13], // centred; zoomed in (wide/flat wave)\n }),\n \"Wave 4\": () =>\n buildPreset({\n speed: 0.0525,\n contrast: 0.969,\n sat: 1.383,\n hueRad: 0.0376991,\n dispX: 0.005,\n dispZ: 0.0212,\n dispAmt: 6.68,\n pos: [206.1, -438, -11.1],\n rotRad: [-0.666018, -0.031416, 0.779115],\n scale: [6.0501, 8.3983, 6.9854],\n twF: [-0.424, 0.024, -1.312],\n twP: [1.81, 0.94, 4.76],\n glow: [1.55, 1.174, 0.972],\n grain: 0.576,\n blur: 0,\n zoom: 0.9316,\n camTarget: [194, -402], // centred on the wave\n twistMotion: true, // variant vertex shader — animated twist-X wobble\n noiseBands: [\n {\n startX: 0.856,\n endX: 1,\n startY: 0,\n endY: 0.913,\n feather: 0.5,\n strength: 0.346,\n frequency: 1018,\n colorAttenuation: 1,\n parabolaPower: 0,\n },\n {\n startX: 0.038,\n endX: 0.538,\n startY: 0.105,\n endY: 1,\n feather: 0.3315,\n strength: 1,\n frequency: 190,\n colorAttenuation: 0,\n parabolaPower: 2.11,\n },\n ],\n }),\n // The dark-background hero: identical geometry/camera to the default hero, but theme\n // \"wireframe\" → the line shader on a dark page background, with grain 1.2. Same palette.\n Wireframe: () => {\n const c = createDefaultConfig();\n c.waves[0].theme = \"wireframe\";\n c.grain = 1.2;\n c.background = \"#0a2540\"; // dark navy page background\n c.transparentBackground = false;\n return c;\n },\n \"Neon Dark Multistrand\": () => {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.theme = \"wireframe\"; // line shader on the near-black background — neon wireframe look\n w.blendMode = \"additive\";\n w.palette = makeStops([\"#00f5d4\", \"#00bbf9\", \"#9b5de5\", \"#f15bb5\", \"#fee440\"]);\n w.creaseLight = 1.0;\n c.background = \"#05060c\";\n c.transparentBackground = false; // fill the dark bg so the neon lines read on black (not the page)\n c.waves = makeWaveSpread(w, 3); // three overlapping neon waves\n c.waveCount = 3;\n return c;\n },\n \"Mesh Gradient\": () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n c.grain = 0.3;\n c.blur = 0.008;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n \"Solar Bloom\": () => {\n // Radial gradient: a warm core blooming out to a deep-indigo edge. usePaletteTexture off so\n // our own stops map along the radial gradCoord instead of sampling the baked hero LUT.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"radial\";\n w.gradientShift = 0.14;\n w.palette = [\n { color: \"#fff3c4\", pos: 0 }, // warm-white core\n { color: \"#ffd166\", pos: 0.22 }, // gold\n { color: \"#ff8c42\", pos: 0.42 }, // orange\n { color: \"#ff5d8f\", pos: 0.62 }, // coral-pink\n { color: \"#a64dff\", pos: 0.82 }, // violet\n { color: \"#241246\", pos: 1 }, // deep indigo edge\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.01;\n // Deep warm radial vignette behind the bloom.\n c.background = \"#0a0714\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"radial\";\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#2a1330\", \"#08040f\"]);\n c.transparentBackground = false;\n return c;\n },\n Holographic: () => {\n // Conic gradient: an iridescent oil-slick sweep. The palette wraps (first ≈ last stop) so\n // the conic seam is invisible.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"conic\";\n w.gradientShift = 0.08;\n w.palette = [\n { color: \"#8ef6e4\", pos: 0 }, // mint (seam)\n { color: \"#6ec3ff\", pos: 0.18 }, // sky\n { color: \"#9b8cff\", pos: 0.36 }, // periwinkle\n { color: \"#ff8ad8\", pos: 0.54 }, // pink\n { color: \"#ffd98e\", pos: 0.72 }, // peach\n { color: \"#a0f0c8\", pos: 0.88 }, // seafoam\n { color: \"#8ef6e4\", pos: 1 }, // mint again (seamless wrap)\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.04;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.12;\n c.grain = 0.28;\n c.blur = 0.01;\n // Subtle deep teal → violet wash behind the iridescence.\n c.background = \"#05060c\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"linear\";\n c.backgroundGradientAngle = 135;\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#04121a\", \"#0a0518\"]);\n c.transparentBackground = false;\n return c;\n },\n Aurora: () => {\n // Mesh gradient: a moody aurora — teals/greens drifting into violet over a night-sky base\n // (distinct from the brighter iOS-style \"Mesh Gradient\").\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a1f3c\", x: 0.08, y: 0.12, influence: 0.62 },\n { color: \"#1fddb0\", x: 0.3, y: 0.7, influence: 0.78 },\n { color: \"#57f5a3\", x: 0.58, y: 0.86, influence: 0.7 },\n { color: \"#3a86ff\", x: 0.82, y: 0.55, influence: 0.62 },\n { color: \"#a15cff\", x: 0.5, y: 0.32, influence: 0.7 },\n { color: \"#071433\", x: 0.92, y: 0.08, influence: 0.6 },\n ];\n w.meshGradientSoftness = 0.72;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.008;\n // Dark night-sky MESH backdrop (also shows off the mesh background type).\n c.background = \"#03060f\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundMeshPoints = [\n { color: \"#02040c\", x: 0.15, y: 0.85, influence: 0.7 },\n { color: \"#08243a\", x: 0.5, y: 0.5, influence: 0.75 },\n { color: \"#0a0f2e\", x: 0.85, y: 0.7, influence: 0.7 },\n { color: \"#04121a\", x: 0.7, y: 0.2, influence: 0.6 },\n { color: \"#000208\", x: 0.12, y: 0.12, influence: 0.6 },\n ];\n c.backgroundMeshSoftness = 0.75;\n c.transparentBackground = false;\n return c;\n },\n Palestine: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"palestine\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1;\n c.grain = 0.35;\n c.background = \"#f2efe8\";\n c.transparentBackground = true;\n return c;\n },\n Spain: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"spain\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.18;\n w.colorSaturation = 1.25;\n w.creaseLight = 1.6; // moderate crease-light: rich crimson without washing to salmon (Hero's is 1.98)\n c.grain = 0.3;\n c.background = \"#1a0608\"; // deep oxblood stage\n c.backgroundMode = \"color\";\n c.transparentBackground = false; // opaque, so the dark stage makes the flag pop\n return c;\n },\n \"Vaporwave Sunset\": () => {\n // The Hero wave re-posed/re-framed, plus the vaporwave palette.\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.position.x = 525;\n w.rotation.x = -0.64 * RAD;\n w.rotation.z = 1.68 * RAD;\n w.paletteSource = \"vaporwave\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.08;\n w.colorSaturation = 1.15;\n w.creaseLight = 1.25;\n c.cameraZoom = 1.1;\n c.cameraTarget = { x: 150, y: 360, z: 0 };\n c.background = \"#09051f\";\n c.transparentBackground = false;\n return c;\n },\n Corkscrew: () => {\n // The helix mode, shown off on its own: `helixRoll` at 1 rolls the ribbon's cross-section in\n // step with the sweep, so the flat strip becomes an auger blade winding around its own length\n // axis, and `helixRadius` lifts that blade off the axis so the turns read as a screw thread\n // rather than a flat twist. No twist at all — this shape is unreachable with twistFrequency,\n // whose expStep angle is monotone and can only ramp once (see the helix docs in config/model).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.helixTurns = 5;\n w.helixRadius = 45;\n w.helixRoll = 1;\n w.helixPhase = 0;\n w.twistFrequency = { x: 0, y: 0, z: 0 };\n w.twistPower = { x: 4, y: 4, z: 2 };\n // A slow swell along the blade so it breathes; the corkscrew itself is static geometry.\n w.displaceAmount = 16;\n w.displaceFrequency = { x: 0.006, y: 0.0008 };\n w.speed = 0.1;\n w.position = { x: 0, y: 0, z: 0 };\n w.rotation = { x: 0, y: 0, z: 12 }; // tilt so it climbs across the frame\n w.scale = { x: 3, y: 3, z: 1.5 };\n // Mesh gradient: the colour field runs along the blade, so each turn picks up a different part\n // of the spectrum instead of the one hue a linear stop ramp would give.\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n // Framed down the axis rather than side-on: the coil reads as a screw receding into the frame,\n // and each turn shows its blade face instead of an edge. cameraDistance is the orbit radius\n // (= |position − target|); the camera is orthographic, so it's the rig's dolly, not the scale —\n // cameraZoom sets that.\n c.cameraPosition = { x: -526.009, y: -285.284, z: -425.489 };\n c.cameraTarget = { x: -95.046, y: -17.053, z: -105.608 };\n c.cameraDistance = 600;\n c.cameraZoom = 1.176;\n c.grain = 0.3;\n c.blur = 0.008;\n c.bloomStrength = 0.35;\n c.bloomRadius = 0.6;\n c.bloomThreshold = 0.55;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n Kaleidoscope: () => {\n const c = PRESETS[\"Wave 3\"]();\n const w = c.waves[0];\n w.paletteSource = \"kaleidoscope\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.12;\n c.grain = 0.5;\n return c;\n },\n};\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import * as THREE from "three";
2
-
3
2
  //#region src/renderer/WaveGeometry.d.ts
4
3
  /**
5
4
  * Base wave geometry — `folded()`: a flat PlaneGeometry folded into a hairpin
@@ -1 +1 @@
1
- {"version":3,"file":"WaveGeometry.js","names":[],"sources":["../../src/renderer/WaveGeometry.ts"],"sourcesContent":["import * as THREE from \"three\";\n\n/** Native plane size for folded() — keep this exact (400) so the vertex\n * shader's displace/twist frequencies (calibrated to this scale) stay faithful. */\nconst NATIVE = 400;\nconst FOLD_X = 16; // |x| < 16 is the semicircular hinge; outside it the two flat arms\nconst SHIFT = NATIVE / 4; // recentre the folded cross-section along x\n\nconst X_AXIS = new THREE.Vector3(1, 0, 0);\nconst Y_AXIS = new THREE.Vector3(0, 1, 0);\n\n/**\n * Base wave geometry — `folded()`: a flat PlaneGeometry folded into a hairpin\n * (sideways-U) cross-section, then stood up so the fold runs along the wave's length.\n *\n * - Each vertex gets a half-thickness `r` (per-vertex math below): tight along the\n * width centreline, flaring toward the long edges.\n * - The strip |x| < FOLD_X becomes a semicircular hinge; the plane's two halves bend\n * around it into parallel arms offset to +r and -r.\n * - Two −90° rotations (about X then Y) orient the U upright and down its length.\n *\n * folded() leaves the U open along one side and hollow at both ends, so at oblique\n * camera angles you could see straight through it. We weld the open side and cap both\n * ends with extra triangles so the mesh is a watertight solid — welding/capping adds\n * faces only, no vertex positions move.\n *\n * All further deformation (displacement, twist, transform) happens in the vertex shader\n * on top of this base. UVs: u along the fold/length, v across the width.\n */\nexport class WaveGeometry {\n readonly geometry: THREE.BufferGeometry;\n private segments = -1;\n\n constructor(segments: number) {\n this.geometry = new THREE.BufferGeometry();\n this.resize(segments);\n }\n\n resize(segments: number): void {\n if (segments === this.segments) return;\n this.segments = segments;\n\n // subX along the fold, subY across the width (twice as dense).\n const subX = THREE.MathUtils.clamp(Math.round(segments), 48, 200);\n const subY = subX * 2;\n\n const plane = new THREE.PlaneGeometry(NATIVE, NATIVE, subX, subY);\n const pos = plane.attributes.position as THREE.BufferAttribute;\n const uv = plane.attributes.uv as THREE.BufferAttribute;\n const v = new THREE.Vector3();\n\n for (let i = 0; i < pos.count; i++) {\n v.fromBufferAttribute(pos, i);\n const uy = uv.getY(i);\n // r: cross-section half-thickness — tight (2) along the width centreline, flaring (4)\n // toward the long edges. The pow() term is a sharp parabolic bump peaking at uv.y = 0.5.\n const r = 4 - 2 * Math.pow(4 * uy * (1 - uy), 9.5);\n\n if (v.x < -FOLD_X) {\n v.z += r; // long arm, at +r\n } else if (v.x < FOLD_X) {\n // semicircular hinge: z sweeps +r → -r, x collapses to the bend\n v.z = Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, 0, Math.PI)) * r;\n v.x =\n Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, -Math.PI / 2, Math.PI / 2)) * r -\n FOLD_X;\n } else {\n v.z -= r; // folded-over arm, mirrored back at -r\n v.x = -v.x;\n }\n\n v.x += SHIFT;\n v.applyAxisAngle(X_AXIS, -Math.PI / 2);\n v.applyAxisAngle(Y_AXIS, -Math.PI / 2);\n pos.setXYZ(i, v.x, v.y, v.z);\n }\n pos.needsUpdate = true;\n\n // Seal the hairpin's OPEN side. folded() leaves the two arm tips unconnected — the\n // plane's u=0 and u=subX edges, which fold to adjacent tips at +r and -r — so at oblique\n // camera angles you can see through the U to the background. Weld those two edges with a\n // strip of triangles, closing the tube. No vertex positions move; this only adds faces\n // over the previously-open seam.\n const cols = subX + 1;\n const srcIdx = plane.getIndex();\n const merged = srcIdx ? Array.from(srcIdx.array as ArrayLike<number>) : [];\n // (a) Weld the U's side opening: the u=0 and u=subX edges fold to adjacent tips at ±r.\n for (let iy = 0; iy < subY; iy++) {\n const a = iy * cols; // (row iy, col 0) — arm-A tip\n const b = (iy + 1) * cols; // (row iy+1, col 0)\n const c = a + subX; // (row iy, col subX) — arm-B tip\n const d = b + subX; // (row iy+1, col subX)\n merged.push(a, c, b, b, c, d);\n }\n // (b) Cap the two length-ends (v=0 and v=subX rows): the folded sheet is a hollow channel\n // open at both ends, so an edge-on camera sees straight through it. Fan-triangulate each\n // end's U cross-section (apex = the col-0 tip) to close it — making the wave a closed solid.\n for (const row of [0, subY]) {\n const apex = row * cols;\n for (let ix = 1; ix < subX; ix++) merged.push(apex, row * cols + ix, row * cols + ix + 1);\n }\n plane.setIndex(merged);\n\n plane.computeVertexNormals();\n\n // Move the baked attributes onto our reusable geometry, then drop the temp.\n this.geometry.setIndex(plane.getIndex());\n this.geometry.setAttribute(\"position\", plane.getAttribute(\"position\"));\n this.geometry.setAttribute(\"uv\", plane.getAttribute(\"uv\"));\n this.geometry.setAttribute(\"normal\", plane.getAttribute(\"normal\"));\n this.geometry.computeBoundingSphere();\n plane.dispose();\n }\n\n dispose(): void {\n this.geometry.dispose();\n }\n}\n"],"mappings":";;;;AAIA,MAAM,SAAS;AACf,MAAM,SAAS;AACf,MAAM,QAAQ,SAAS;AAEvB,MAAM,SAAS,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AACxC,MAAM,SAAS,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;;;;;;;;;;;;;;;;;;;AAoBxC,IAAa,eAAb,MAA0B;CACxB;CACA,WAAmB;CAEnB,YAAY,UAAkB;EAC5B,KAAK,WAAW,IAAI,MAAM,eAAe;EACzC,KAAK,OAAO,QAAQ;CACtB;CAEA,OAAO,UAAwB;EAC7B,IAAI,aAAa,KAAK,UAAU;EAChC,KAAK,WAAW;EAGhB,MAAM,OAAO,MAAM,UAAU,MAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG;EAChE,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,IAAI,MAAM,cAAc,QAAQ,QAAQ,MAAM,IAAI;EAChE,MAAM,MAAM,MAAM,WAAW;EAC7B,MAAM,KAAK,MAAM,WAAW;EAC5B,MAAM,IAAI,IAAI,MAAM,QAAQ;EAE5B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,OAAO,KAAK;GAClC,EAAE,oBAAoB,KAAK,CAAC;GAC5B,MAAM,KAAK,GAAG,KAAK,CAAC;GAGpB,MAAM,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,GAAG;GAEjD,IAAI,EAAE,IAAI,KACR,EAAE,KAAK;QACF,IAAI,EAAE,IAAI,QAAQ;IAEvB,EAAE,IAAI,KAAK,IAAI,MAAM,UAAU,UAAU,EAAE,GAAG,KAAS,QAAQ,GAAG,KAAK,EAAE,CAAC,IAAI;IAC9E,EAAE,IACA,KAAK,IAAI,MAAM,UAAU,UAAU,EAAE,GAAG,KAAS,QAAQ,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,IAAI,IACvF;GACJ,OAAO;IACL,EAAE,KAAK;IACP,EAAE,IAAI,CAAC,EAAE;GACX;GAEA,EAAE,KAAK;GACP,EAAE,eAAe,QAAQ,CAAC,KAAK,KAAK,CAAC;GACrC,EAAE,eAAe,QAAQ,CAAC,KAAK,KAAK,CAAC;GACrC,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;EAC7B;EACA,IAAI,cAAc;EAOlB,MAAM,OAAO,OAAO;EACpB,MAAM,SAAS,MAAM,SAAS;EAC9B,MAAM,SAAS,SAAS,MAAM,KAAK,OAAO,KAA0B,IAAI,CAAC;EAEzE,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,MAAM;GAChC,MAAM,IAAI,KAAK;GACf,MAAM,KAAK,KAAK,KAAK;GACrB,MAAM,IAAI,IAAI;GACd,MAAM,IAAI,IAAI;GACd,OAAO,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;EAC9B;EAIA,KAAK,MAAM,OAAO,CAAC,GAAG,IAAI,GAAG;GAC3B,MAAM,OAAO,MAAM;GACnB,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK,CAAC;EAC1F;EACA,MAAM,SAAS,MAAM;EAErB,MAAM,qBAAqB;EAG3B,KAAK,SAAS,SAAS,MAAM,SAAS,CAAC;EACvC,KAAK,SAAS,aAAa,YAAY,MAAM,aAAa,UAAU,CAAC;EACrE,KAAK,SAAS,aAAa,MAAM,MAAM,aAAa,IAAI,CAAC;EACzD,KAAK,SAAS,aAAa,UAAU,MAAM,aAAa,QAAQ,CAAC;EACjE,KAAK,SAAS,sBAAsB;EACpC,MAAM,QAAQ;CAChB;CAEA,UAAgB;EACd,KAAK,SAAS,QAAQ;CACxB;AACF"}
1
+ {"version":3,"file":"WaveGeometry.js","names":[],"sources":["../../src/renderer/WaveGeometry.ts"],"sourcesContent":["import * as THREE from \"three\";\n\n/** Native plane size for folded() — keep this exact (400) so the vertex\n * shader's displace/twist frequencies (calibrated to this scale) stay faithful. */\nconst NATIVE = 400;\nconst FOLD_X = 16; // |x| < 16 is the semicircular hinge; outside it the two flat arms\nconst SHIFT = NATIVE / 4; // recentre the folded cross-section along x\n\nconst X_AXIS = new THREE.Vector3(1, 0, 0);\nconst Y_AXIS = new THREE.Vector3(0, 1, 0);\n\n/**\n * Local-Z centre of the folded ribbon's width. The fold collapses x ∈ [-NATIVE/2, NATIVE/2] onto a\n * single arm and SHIFT recentres it, which lands the width at [-100, 84] rather than symmetric\n * about 0 — so a rotation about local X through the ORIGIN would swing the ribbon's two long edges\n * to radii 100 and 84 (a visibly lopsided helix). The vertex shader's helix roll rotates about this\n * line instead, so both edges come out at equal radius.\n */\nexport const RIBBON_Z_CENTER = (SHIFT - NATIVE / 2 + (SHIFT - FOLD_X)) / 2;\n\n/**\n * Base wave geometry — `folded()`: a flat PlaneGeometry folded into a hairpin\n * (sideways-U) cross-section, then stood up so the fold runs along the wave's length.\n *\n * - Each vertex gets a half-thickness `r` (per-vertex math below): tight along the\n * width centreline, flaring toward the long edges.\n * - The strip |x| < FOLD_X becomes a semicircular hinge; the plane's two halves bend\n * around it into parallel arms offset to +r and -r.\n * - Two −90° rotations (about X then Y) orient the U upright and down its length.\n *\n * folded() leaves the U open along one side and hollow at both ends, so at oblique\n * camera angles you could see straight through it. We weld the open side and cap both\n * ends with extra triangles so the mesh is a watertight solid — welding/capping adds\n * faces only, no vertex positions move.\n *\n * All further deformation (displacement, twist, transform) happens in the vertex shader\n * on top of this base. UVs: u along the fold/length, v across the width.\n */\nexport class WaveGeometry {\n readonly geometry: THREE.BufferGeometry;\n private segments = -1;\n\n constructor(segments: number) {\n this.geometry = new THREE.BufferGeometry();\n this.resize(segments);\n }\n\n resize(segments: number): void {\n if (segments === this.segments) return;\n this.segments = segments;\n\n // subX along the fold, subY across the width (twice as dense).\n const subX = THREE.MathUtils.clamp(Math.round(segments), 48, 200);\n const subY = subX * 2;\n\n const plane = new THREE.PlaneGeometry(NATIVE, NATIVE, subX, subY);\n const pos = plane.attributes.position as THREE.BufferAttribute;\n const uv = plane.attributes.uv as THREE.BufferAttribute;\n const v = new THREE.Vector3();\n\n for (let i = 0; i < pos.count; i++) {\n v.fromBufferAttribute(pos, i);\n const uy = uv.getY(i);\n // r: cross-section half-thickness — tight (2) along the width centreline, flaring (4)\n // toward the long edges. The pow() term is a sharp parabolic bump peaking at uv.y = 0.5.\n const r = 4 - 2 * Math.pow(4 * uy * (1 - uy), 9.5);\n\n if (v.x < -FOLD_X) {\n v.z += r; // long arm, at +r\n } else if (v.x < FOLD_X) {\n // semicircular hinge: z sweeps +r → -r, x collapses to the bend\n v.z = Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, 0, Math.PI)) * r;\n v.x =\n Math.cos(THREE.MathUtils.mapLinear(v.x, -FOLD_X, FOLD_X, -Math.PI / 2, Math.PI / 2)) * r -\n FOLD_X;\n } else {\n v.z -= r; // folded-over arm, mirrored back at -r\n v.x = -v.x;\n }\n\n v.x += SHIFT;\n v.applyAxisAngle(X_AXIS, -Math.PI / 2);\n v.applyAxisAngle(Y_AXIS, -Math.PI / 2);\n pos.setXYZ(i, v.x, v.y, v.z);\n }\n pos.needsUpdate = true;\n\n // Seal the hairpin's OPEN side. folded() leaves the two arm tips unconnected — the\n // plane's u=0 and u=subX edges, which fold to adjacent tips at +r and -r — so at oblique\n // camera angles you can see through the U to the background. Weld those two edges with a\n // strip of triangles, closing the tube. No vertex positions move; this only adds faces\n // over the previously-open seam.\n const cols = subX + 1;\n const srcIdx = plane.getIndex();\n const merged = srcIdx ? Array.from(srcIdx.array as ArrayLike<number>) : [];\n // (a) Weld the U's side opening: the u=0 and u=subX edges fold to adjacent tips at ±r.\n for (let iy = 0; iy < subY; iy++) {\n const a = iy * cols; // (row iy, col 0) — arm-A tip\n const b = (iy + 1) * cols; // (row iy+1, col 0)\n const c = a + subX; // (row iy, col subX) — arm-B tip\n const d = b + subX; // (row iy+1, col subX)\n merged.push(a, c, b, b, c, d);\n }\n // (b) Cap the two length-ends (v=0 and v=subX rows): the folded sheet is a hollow channel\n // open at both ends, so an edge-on camera sees straight through it. Fan-triangulate each\n // end's U cross-section (apex = the col-0 tip) to close it — making the wave a closed solid.\n for (const row of [0, subY]) {\n const apex = row * cols;\n for (let ix = 1; ix < subX; ix++) merged.push(apex, row * cols + ix, row * cols + ix + 1);\n }\n plane.setIndex(merged);\n\n plane.computeVertexNormals();\n\n // Move the baked attributes onto our reusable geometry, then drop the temp.\n this.geometry.setIndex(plane.getIndex());\n this.geometry.setAttribute(\"position\", plane.getAttribute(\"position\"));\n this.geometry.setAttribute(\"uv\", plane.getAttribute(\"uv\"));\n this.geometry.setAttribute(\"normal\", plane.getAttribute(\"normal\"));\n this.geometry.computeBoundingSphere();\n plane.dispose();\n }\n\n dispose(): void {\n this.geometry.dispose();\n }\n}\n"],"mappings":";;;;AAIA,MAAM,SAAS;AACf,MAAM,SAAS;AACf,MAAM,QAAQ,SAAS;AAEvB,MAAM,SAAS,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;AACxC,MAAM,SAAS,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;;;;;;;;;;;;;;;;;;;AA6BxC,IAAa,eAAb,MAA0B;CACxB;CACA,WAAmB;CAEnB,YAAY,UAAkB;EAC5B,KAAK,WAAW,IAAI,MAAM,eAAe;EACzC,KAAK,OAAO,QAAQ;CACtB;CAEA,OAAO,UAAwB;EAC7B,IAAI,aAAa,KAAK,UAAU;EAChC,KAAK,WAAW;EAGhB,MAAM,OAAO,MAAM,UAAU,MAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,GAAG;EAChE,MAAM,OAAO,OAAO;EAEpB,MAAM,QAAQ,IAAI,MAAM,cAAc,QAAQ,QAAQ,MAAM,IAAI;EAChE,MAAM,MAAM,MAAM,WAAW;EAC7B,MAAM,KAAK,MAAM,WAAW;EAC5B,MAAM,IAAI,IAAI,MAAM,QAAQ;EAE5B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,OAAO,KAAK;GAClC,EAAE,oBAAoB,KAAK,CAAC;GAC5B,MAAM,KAAK,GAAG,KAAK,CAAC;GAGpB,MAAM,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,GAAG;GAEjD,IAAI,EAAE,IAAI,KACR,EAAE,KAAK;QACF,IAAI,EAAE,IAAI,QAAQ;IAEvB,EAAE,IAAI,KAAK,IAAI,MAAM,UAAU,UAAU,EAAE,GAAG,KAAS,QAAQ,GAAG,KAAK,EAAE,CAAC,IAAI;IAC9E,EAAE,IACA,KAAK,IAAI,MAAM,UAAU,UAAU,EAAE,GAAG,KAAS,QAAQ,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,IAAI,IACvF;GACJ,OAAO;IACL,EAAE,KAAK;IACP,EAAE,IAAI,CAAC,EAAE;GACX;GAEA,EAAE,KAAK;GACP,EAAE,eAAe,QAAQ,CAAC,KAAK,KAAK,CAAC;GACrC,EAAE,eAAe,QAAQ,CAAC,KAAK,KAAK,CAAC;GACrC,IAAI,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;EAC7B;EACA,IAAI,cAAc;EAOlB,MAAM,OAAO,OAAO;EACpB,MAAM,SAAS,MAAM,SAAS;EAC9B,MAAM,SAAS,SAAS,MAAM,KAAK,OAAO,KAA0B,IAAI,CAAC;EAEzE,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,MAAM;GAChC,MAAM,IAAI,KAAK;GACf,MAAM,KAAK,KAAK,KAAK;GACrB,MAAM,IAAI,IAAI;GACd,MAAM,IAAI,IAAI;GACd,OAAO,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;EAC9B;EAIA,KAAK,MAAM,OAAO,CAAC,GAAG,IAAI,GAAG;GAC3B,MAAM,OAAO,MAAM;GACnB,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,MAAM,OAAO,IAAI,MAAM,OAAO,KAAK,CAAC;EAC1F;EACA,MAAM,SAAS,MAAM;EAErB,MAAM,qBAAqB;EAG3B,KAAK,SAAS,SAAS,MAAM,SAAS,CAAC;EACvC,KAAK,SAAS,aAAa,YAAY,MAAM,aAAa,UAAU,CAAC;EACrE,KAAK,SAAS,aAAa,MAAM,MAAM,aAAa,IAAI,CAAC;EACzD,KAAK,SAAS,aAAa,UAAU,MAAM,aAAa,QAAQ,CAAC;EACjE,KAAK,SAAS,sBAAsB;EACpC,MAAM,QAAQ;CAChB;CAEA,UAAgB;EACd,KAAK,SAAS,QAAQ;CACxB;AACF"}
@@ -2,7 +2,6 @@ import { CameraFit, StudioConfig, WaveConfig } from "../config/model.js";
2
2
  import { WaveGeometry } from "./WaveGeometry.js";
3
3
  import { InteractionController } from "./interaction.js";
4
4
  import * as THREE from "three";
5
-
6
5
  //#region src/renderer/WaveRenderer.d.ts
7
6
  /** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
8
7
  * framed by mapping this FRAME_W × FRAME_H rectangle (centred on cameraTarget) onto the canvas,
@@ -64,7 +63,8 @@ declare class WavePalette {
64
63
  type Wave = {
65
64
  mesh: THREE.Mesh;
66
65
  material: THREE.ShaderMaterial;
67
- geometry: WaveGeometry; /** This wave's own 2D palette texture + optional video. */
66
+ geometry: WaveGeometry;
67
+ /** This wave's own 2D palette texture + optional video. */
68
68
  palette: WavePalette;
69
69
  };
70
70
  /** Convert an sRGB hex string to a linear-space RGB vector (three's ColorManagement does the