@wave3d/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/model.d.ts +209 -6
- package/dist/config/model.js +85 -3
- package/dist/config/model.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/presets.js +295 -0
- package/dist/presets.js.map +1 -1
- package/dist/renderer/WaveGeometry.js +21 -0
- package/dist/renderer/WaveGeometry.js.map +1 -1
- package/dist/renderer/WaveRenderer.d.ts +62 -0
- package/dist/renderer/WaveRenderer.js +349 -10
- package/dist/renderer/WaveRenderer.js.map +1 -1
- package/dist/renderer/WaveRendererGPU.js +32 -2
- package/dist/renderer/WaveRendererGPU.js.map +1 -1
- package/dist/renderer/interaction.js +12 -0
- package/dist/renderer/interaction.js.map +1 -1
- package/dist/renderer/particleField.js +26 -2
- package/dist/renderer/particleField.js.map +1 -1
- package/dist/renderer/particleFieldGPU.js +6 -0
- package/dist/renderer/particleFieldGPU.js.map +1 -1
- package/dist/renderer/shaders.js +748 -13
- package/dist/renderer/shaders.js.map +1 -1
- package/dist/renderer/tsl/dissolve.js +56 -0
- package/dist/renderer/tsl/dissolve.js.map +1 -0
- package/dist/renderer/tsl/particleMaterial.js +46 -8
- package/dist/renderer/tsl/particleMaterial.js.map +1 -1
- package/dist/renderer/tsl/uniforms.js +40 -1
- package/dist/renderer/tsl/uniforms.js.map +1 -1
- package/dist/renderer/tsl/waveMaterial.js +275 -26
- package/dist/renderer/tsl/waveMaterial.js.map +1 -1
- package/dist/renderer/tsl/waveShape.js +31 -10
- package/dist/renderer/tsl/waveShape.js.map +1 -1
- package/dist/renderer/wavePath.js +189 -0
- package/dist/renderer/wavePath.js.map +1 -0
- package/dist/shell/createWave.d.ts +23 -3
- package/dist/shell/createWave.js +5 -4
- package/dist/shell/createWave.js.map +1 -1
- package/dist/shell/probe.d.ts +28 -0
- package/dist/shell/probe.js +58 -11
- package/dist/shell/probe.js.map +1 -1
- package/dist/standalone/wave3d.standalone.js +3401 -2108
- package/dist/standalone/wave3d.standalone.webgpu.js +7372 -5848
- package/dist/standalone.d.ts +2 -2
- package/dist/standalone.js +2 -2
- package/dist/studio/StudioWaveRenderer.d.ts +115 -8
- package/dist/studio/StudioWaveRenderer.js +588 -14
- package/dist/studio/StudioWaveRenderer.js.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/randomize.js +0 -1
- package/dist/studio/randomize.js.map +1 -1
- package/package.json +1 -1
- package/skills/wave3d/SKILL.md +142 -5
package/dist/config/model.js.map
CHANGED
|
@@ -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, softened by `feather`, the fiber streaks\n * are overridden — strength, frequency (density), colourAttenuation (how much the local\n * colour suppresses them), and the edge-weighting parabolaPower. Lets the fibers vary\n * per region instead of uniform.\n *\n * Mind the axes, which are the reverse of what the names suggest: the bounds gate on uv,\n * and uv.x is the folded WIDTH while uv.y is the LENGTH (see WaveGeometry's UV AXES note).\n * So startX..endX span the SHORT axis and startY..endY run end to end.\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) along the gradient ramp, whose\n * direction on the ribbon is set by `gradientType` / `gradientAngle`. */\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 two ENDS — it smoothsteps on uv.y, which is the length, not the\n * long edges. 0.1 = the original hardcoded value; smaller = razor-crisp graphic ribbons,\n * 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 /** Radial fan (optional): sweep the ribbon's length into a plume/peacock spread from the local\n * origin. The three twists and the helix can't reach it — this maps the ribbon to polar so the\n * combed fibers ({@link fiberCount}) read as the individual radial strands. Placement is the wave's\n * `position` transform (there is no separate pivot). Runs AFTER displace/helix/twist, behind\n * `#ifdef RADIAL`, so `radialAmount` 0 leaves the block uncompiled and the wave byte-identical. */\n radialAmount?: number; // 0..1 gate + blend (0 = off / identity)\n radialArc?: number; // fan spread, degrees\n radialSpread?: number; // along-length → radius scale\n radialRadius?: number; // source / inner radius (world units, pre-scale)\n radialCenter?: number; // base angle, degrees\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 /** Optional per-wave particle / dust field emitted off THIS wave's deformed surface / edge.\n * ABSENT ⇒ off (no THREE.Points for this wave, byte-identical). See {@link ParticlesConfig}. */\n particles?: ParticlesConfig;\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 \"tiltX\",\n \"tiltY\",\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 | \"tiltX\" // device tilt, the way a ball would roll: 1 = right edge down. 0.5 = neutral pose\n | \"tiltY\" // device tilt, the way a ball would roll: 1 = bottom edge down. 0.5 = neutral pose\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 /** Tuning for the device-tilt input. OPTIONAL even on a scene that uses tilt: a `tiltX` / `tiltY`\n * binding is what arms the sensor, and this only shapes how it reads. */\n tilt?: TiltConfig;\n /** Input→param bindings driving SCENE params (timeOffset, cameraZoom, blur, grain). */\n bindings?: SceneInteractionBinding[];\n}\n\n/**\n * Device tilt (a phone or tablet's orientation sensor) as an interaction input, feeding the `tiltX`\n * and `tiltY` sources. It is the one input a phone has that a cursor-authored scene doesn't: a page\n * whose bindings all read `pointerX` / `pointerY` is completely inert on a phone, and a couple of\n * tilt bindings — or `pointer: true` below — is what brings it back.\n *\n * Both axes read the way a ball would roll on the screen: `tiltX` → 1 as the right edge drops,\n * `tiltY` → 1 as the bottom edge drops, and both rest at 0.5 in whatever pose the reader was\n * already holding the device in when the first reading landed.\n *\n * This block is TUNING, not the on-switch: binding anything to `tiltX` / `tiltY` arms the sensor by\n * itself, the same way `pointerX` needs no \"pointer\" block. A scene that binds neither (and doesn't\n * set `pointer` below) attaches no `deviceorientation` listener at all.\n *\n * ARMED is not the same as LIVE, and on iOS it never becomes live on its own. Safari gates the\n * sensor behind a modal permission dialog, and nothing here opens one: a tilt-bound scene on an\n * iPhone reads 0.5 on both axes and renders exactly as it would with no tilt block at all. That is\n * deliberate — a background effect is not worth interrupting a reader to ask for a sensor. Design\n * for tilt as an enhancement that some phones simply don't get, the way you would a hover state.\n *\n * `renderer.enableTilt()` (or the shell handle's / the element's) is the explicit opt-in for a page\n * that has decided otherwise — an interactive piece a reader came to play with, where a tap that\n * opens the dialog is part of the deal. `renderer.tiltStatus()` reports `\"prompt\"` on a gated\n * platform, which is information, not an instruction to build a permission button.\n */\nexport interface TiltConfig {\n /** Degrees away from the neutral pose that reach the 0 / 1 ends. Default 25 — about the range of\n * a wrist, not of a whole arm. Smaller = a twitchier scene that reacts to a nudge. */\n range?: number;\n /** Follow smoothing, seconds. Default 0.18: sensor data is noisy and a still hand still jitters,\n * so this is slower than the pointer's 0.12. */\n smoothing?: number;\n /** Flip the horizontal / vertical direction (which way is \"up\" is a property of the scene). */\n invertX?: boolean;\n invertY?: boolean;\n /**\n * Also drive the shared CURSOR from tilt, so a scene authored for `pointerX` / `pointerY` — and\n * every wave's hover field — comes alive on a phone that has no cursor at all, without authoring\n * a second set of bindings. A real pointer always wins: this only fills in while none is on the\n * element. Default false.\n */\n pointer?: boolean;\n}\n\n/**\n * A WAVE's field of additive GPU sprites (dust / sparkle). Lives on {@link WaveConfig} — particles\n * belong to a wave, not the scene. ABSENT ⇒ off: no THREE.Points node is created for that wave and its\n * render is byte-identical; present ⇒ {@link normalizeParticles} clamps it. Every particle's motion is a\n * pure function of `uTime` + a per-particle seed baked from `seed`, so it is deterministic (timeOffset\n * scrub / loopSeconds / paused all hold). Particles spawn on the OWNING wave's DEFORMED surface / edge\n * (via the shared waveShape) and drift outward from the wave centre.\n */\n/** How each particle sprite is drawn (a per-field render style, not per-particle). All but\n * \"sprite\" are drawn procedurally from `gl_PointCoord`; \"sprite\" samples {@link\n * ParticlesConfig.spriteUrl} and falls back to \"glitter\" until that image has rasterized. */\nexport type ParticleShape = \"glitter\" | \"soft\" | \"ring\" | \"star\" | \"streak\" | \"sprite\";\nexport const PARTICLE_SHAPES: readonly ParticleShape[] = [\n \"glitter\",\n \"soft\",\n \"ring\",\n \"star\",\n \"streak\",\n \"sprite\",\n];\n\nexport interface ParticlesConfig {\n count: number; // total sprites (clamped in normalizeParticles)\n size: number; // base sprite size, px\n seed: number; // PRNG seed → reproducible layout\n sizeJitter?: number; // 0..1 per-particle size variance\n color?: string; // sprite colour (warm gold default)\n /** Second sprite colour — particles interpolate between `color` and this by their seed (two-tone dust). */\n color2?: string;\n life?: number; // seconds per birth→death cycle\n /** Motion-speed multiplier for the dust: how fast particles cycle + drift (1 = default, 0 = frozen).\n * Independent of the wave's own `speed` (which animates the surface the dust rides). Under a seamless\n * `loopSeconds` it snaps to a whole number of cycles so the loop stays seamless. */\n speed?: number;\n twinkle?: number; // 0..1 brightness flicker\n /** Where on the wave particles spawn: 0 = across the whole SURFACE, 1 = the outer rim / EDGE only. */\n edgeBias?: number;\n /** How far particles drift outward from the wave centre as they age (world units). */\n drift?: number;\n /** −1..1 skew of the spawn along the edge width toward one flank (0 = even, −1 → one side, +1 → other). */\n bias?: number;\n /** Screen-vertical buoyancy over a lifetime, world units: + rises (embers), − falls (snow / ash). */\n rise?: number;\n /** Orbit around the wave centre in the screen plane, turns per lifetime (swirls the dust). */\n swirl?: number;\n /** Curl-noise turbulence: particles meander (fireflies / motes) instead of moving in straight lines. */\n wander?: number;\n /** Sprite render style. Default \"glitter\" (the soft round additive disc). */\n shape?: ParticleShape;\n /**\n * Artwork for `shape: \"sprite\"` — an SVG (or raster) `data:` URI or URL, rasterized ONCE into a\n * square texture shared by every particle in the field, so the cost is one texture per field and\n * not per particle. Ignored unless `shape` is \"sprite\"; until it has loaded the field draws\n * \"glitter\", so a slow or broken image degrades instead of blanking.\n *\n * The sprite is TINTED by `color` / `color2` (multiplied), so the field's colour knobs keep\n * working — supply WHITE artwork to take the tint literally, or coloured artwork to modulate it.\n * Non-square art is letterboxed, because a point sprite is always square.\n *\n * Prefer SVG: it is a fraction of the bytes of an equivalent PNG, and the whole config (this\n * string included) is embedded in save-states and share links.\n */\n spriteUrl?: string;\n /**\n * How hard the cursor shoves dust that has already drifted OFF the surface, as a multiple of the\n * displacement the wave's own {@link WaveHoverConfig} field applies. Default 1; 0 = airborne motes\n * ignore the pointer. Only ever reads when the owning wave has a pointer field of its own — with\n * no `interaction.hover` there is nothing to shove with, and the point program compiles without\n * the pointer path at all.\n *\n * Dust still ATTACHED to the surface is not governed by this: a mote sitting on the ribbon always\n * takes the ribbon's own displacement (weighted by how far it has drifted), because otherwise a\n * cursor poke would lift the surface out from under its own glitter.\n */\n pointerShove?: number;\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 // Radial off: amount 0 leaves the RADIAL block uncompiled (see waveDefines).\n radialAmount: 0,\n radialArc: 160,\n radialSpread: 1,\n radialRadius: 40,\n radialCenter: 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 // particles is deliberately absent (off = byte-identical).\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 (!Number.isFinite(s.radialAmount)) s.radialAmount = 0;\n if (!Number.isFinite(s.radialArc)) s.radialArc = 160;\n if (!Number.isFinite(s.radialSpread)) s.radialSpread = 1;\n if (!Number.isFinite(s.radialRadius)) s.radialRadius = 40;\n if (!Number.isFinite(s.radialCenter)) s.radialCenter = 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 if (s.particles) normalizeParticles(s); // present-only; absence = no field for this wave\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 // particles is present-only (like interaction): NOT backfilled here — absence is off.\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.tilt) {\n const t = it.tilt;\n if (t.range !== undefined) t.range = clampNumber(t.range, 1, 90, 25);\n if (t.smoothing !== undefined) t.smoothing = clampNumber(t.smoothing, 0, 2, 0.18);\n }\n if (it.bindings !== undefined) {\n it.bindings = cleanBindings<SceneInteractionTarget>(it.bindings, SCENE_TARGET_NAMES);\n }\n}\n\n/**\n * Present-only normalizer for the scene PARTICLES block: clamp the numerics that are present and\n * repair the required fields, leaving absent optionals absent (so the block stays lean). NEVER call\n * when the block is absent — absence is off and byte-identical (ensureStudioConfig gates on presence).\n */\nexport function normalizeParticles(wave: WaveConfig): void {\n const p = wave.particles;\n if (!p) return;\n p.count = clampNumber(p.count, 0, 40000, 0);\n p.size = clampNumber(p.size, 0, 200, 2);\n p.seed = num(p.seed, 0);\n if (p.sizeJitter !== undefined) p.sizeJitter = clampNumber(p.sizeJitter, 0, 1, 0);\n if (p.color !== undefined && typeof p.color !== \"string\") p.color = \"#ffcf8a\";\n if (p.life !== undefined) p.life = clampNumber(p.life, 0.1, 60, 6);\n if (p.speed !== undefined) p.speed = clampNumber(p.speed, 0, 8, 1);\n if (p.twinkle !== undefined) p.twinkle = clampNumber(p.twinkle, 0, 1, 0);\n if (p.color2 !== undefined && typeof p.color2 !== \"string\") p.color2 = \"#ffcf8a\";\n if (p.edgeBias !== undefined) p.edgeBias = clampNumber(p.edgeBias, 0, 1, 1);\n if (p.drift !== undefined) p.drift = num(p.drift, 0);\n if (p.bias !== undefined) p.bias = clampNumber(p.bias, -1, 1, 0);\n if (p.rise !== undefined) p.rise = num(p.rise, 0);\n if (p.swirl !== undefined) p.swirl = num(p.swirl, 0);\n if (p.wander !== undefined) p.wander = num(p.wander, 0);\n if (p.shape !== undefined && !PARTICLE_SHAPES.includes(p.shape)) p.shape = \"glitter\";\n // Untrusted configs (share links / imported JSON) reach here — keep the url a string, but do not\n // validate the scheme: the renderer only ever hands it to an <img>, which sandboxes SVG scripts.\n if (p.spriteUrl !== undefined && typeof p.spriteUrl !== \"string\") delete p.spriteUrl;\n if (p.pointerShove !== undefined) p.pointerShove = clampNumber(p.pointerShove, 0, 4, 1);\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 // each wave's normalizeWave runs normalizeWaveInteraction + normalizeParticles (both present-only)\n config.waves.forEach(normalizeWave);\n config.waveCount = config.waves.length;\n // Present-only: a config without a scene `interaction` block is left untouched (\"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/**\n * One control point of a {@link WaveConfig.path}: where the ribbon's centre passes, how wide it is\n * there, and how far its cross-section has rotated. `width` 1 is the ribbon's natural width; `twist`\n * is in degrees. Both are optional and default to 1 / 0.\n */\nexport interface PathPoint {\n x: number;\n y: number;\n z: number;\n width?: number;\n twist?: number;\n}\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, softened by `feather`, the fiber streaks\n * are overridden — strength, frequency (density), colourAttenuation (how much the local\n * colour suppresses them), and the edge-weighting parabolaPower. Lets the fibers vary\n * per region instead of uniform.\n *\n * Mind the axes, which are the reverse of what the names suggest: the bounds gate on uv,\n * and uv.x is the folded WIDTH while uv.y is the LENGTH (see WaveGeometry's UV AXES note).\n * So startX..endX span the SHORT axis and startY..endY run end to end.\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) along the gradient ramp, whose\n * direction on the ribbon is set by `gradientType` / `gradientAngle`. */\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 /** What to call this wave in the studio, if \"Wave 3\" is not enough. Absent ⇒ that numbering. */\n name?: string;\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 two ENDS — it smoothsteps on uv.y, which is the length, not the\n * long edges. 0.1 = the original hardcoded value; smaller = razor-crisp graphic ribbons,\n * larger = soft vapor. Both themes honour it: on the wireframe it is what stops a sweep ending\n * at a flat end-cap that reads as a straight cut drawn across the strands. */\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 /** Radial fan (optional): sweep the ribbon's length into a plume/peacock spread from the local\n * origin. The three twists and the helix can't reach it — this maps the ribbon to polar so the\n * combed fibers ({@link fiberCount}) read as the individual radial strands. Placement is the wave's\n * `position` transform (there is no separate pivot). Runs AFTER displace/helix/twist, behind\n * `#ifdef RADIAL`, so `radialAmount` 0 leaves the block uncompiled and the wave byte-identical. */\n radialAmount?: number; // 0..1 gate + blend (0 = off / identity)\n radialArc?: number; // fan spread, degrees\n radialSpread?: number; // along-length → radius scale\n radialRadius?: number; // source / inner radius (world units, pre-scale)\n radialCenter?: number; // base angle, degrees\n /** Lift the fan out of its own plane as it spreads, turning the flat plume into a CONE — a\n * trumpet / morning-glory mouth whose combed strands run down the slant into the throat, which is\n * the one thing neither the twists nor the helix can reach (a helix carries the ribbon around an\n * axis, but its WIDTH never follows the slant). Measured as lift per ribbon-length of radius:\n * 0 = the flat fan (the default, byte-identical), ~0.6 a wide mouth, ~1.4 a narrow horn.\n * Negative cones the other way. Inert unless `radialAmount` > 0. */\n radialCone?: number;\n /** Degrees of ANGLE the band gains along its own length. The fan's angle otherwise comes from uv.x\n * alone, so every arm runs straight out from the throat; radius already grows with uv.y, and\n * letting angle grow with it too is exactly what turns a straight arm into a SPIRAL one that\n * curves around the throat. 0 = straight (the default, byte-identical); 150 wraps most of a\n * half-turn. Negative spirals the other way. Inert unless `radialAmount` > 0. */\n radialSwirl?: number;\n /**\n * PATH — the ribbon's centreline, as control points it is swept along. Absent ⇒ the straight\n * centreline the folded geometry is born with (byte-identical: the shader block is not compiled).\n *\n * This is the shape control the others cannot substitute for. The twists rotate a ribbon whose\n * centreline is fixed, the helix carries that fixed centreline around an axis, the radial fan\n * splays it — so none of them can make a ribbon that changes direction more than once, crosses\n * itself, or is wide here and narrow there. A path can, because it IS the centreline.\n *\n * Points are in the wave's LOCAL space, the same units the geometry uses: the un-pathed ribbon\n * runs from x −200 to +200 along its length, so `straightPath()` reproduces it. They are swept by\n * ARC LENGTH with a parallel-transported frame, which is what keeps the strand comb even however\n * the points are dragged and stops the ribbon snapping through inflections.\n *\n * Per point, `width` scales the ribbon's width there (0.1 is a throat, 2 a flare — this is what a\n * separate \"pinch\" knob would otherwise be) and `twist` rotates its cross-section in degrees.\n * Both interpolate smoothly between points.\n */\n path?: PathPoint[];\n\n // Material (\"solid\" surface · \"wireframe\" line shader · \"glass\" refracting sheet)\n theme?: \"solid\" | \"wireframe\" | \"glass\";\n\n // ---- glass theme ----\n /** Glass only: how far the ribbon bends what is behind it, in PIXELS at the silhouette. The bend\n * is strongest where the surface turns away from the camera and falls to nothing face-on, which\n * is what gives a sheet its edge compression. 0 is a clear pane. */\n glassStrength?: number;\n /** Glass only: per-channel split of that bend (dispersion). Past ~1 it reads as an oil sheen. */\n glassChroma?: number;\n /** Glass only: 0 clear · 1 frosted. Blurs the backdrop AT the refracted position, so the frost\n * rides the bend rather than sitting flat under it. The scatter radius grows with the square of\n * this, and the taps are skipped while it is under half a pixel (below ~0.1), where they could\n * only average back to the sample they surround. */\n glassFrost?: number;\n /** Glass only: strength of the edge glint. It ADDS light over a dark backdrop and DARKENS over a\n * bright one, which is what keeps a rim visible on white paper. */\n glassSpec?: number;\n /** Glass only: how far the interior pulls toward mid-grey — legibility for anything read through\n * the sheet, and the haze that separates glass from a clear hole. */\n glassVibrancy?: number;\n /** Glass only: how much of the wave's own palette colour tints the glass (0 = colourless).\n * Deliberately LOW by default. Glass reads as glass because of what is behind it being bent, not\n * because the sheet carries colour — push this up and it stops looking like glass and starts\n * looking like a filled material that happens to be shiny. */\n glassTint?: number;\n /** Glass only: HALF the optical path at normal incidence — the sheet's thickness. With density it\n * sets how saturated the transmitted colour gets. This is what makes glass a material rather\n * than a window: the colour comes from the ribbon's own palette absorbed over its own thickness,\n * so it reads as glass with nothing behind it at all. */\n glassPath?: number;\n /** Glass only: absorption coefficient. High = deep, saturated glass; low = barely tinted. */\n glassDensity?: number;\n /** Glass only: edge whitening. The band is deliberately WIDE — a narrow one is thinner than a\n * pixel on a thin ribbon and the knob does nothing at all. */\n glassRim?: number;\n /** Glass only: thin-film iridescence on the reflection, rim and specular — never on the\n * transmission, which would read as dye rather than as a film. */\n glassIrid?: number;\n /** Glass only: optical film thickness in nm; 300–500 is the soap-bubble band. */\n glassFilmNm?: number;\n /** Glass only: index of refraction, used by the fresnel and the film. */\n glassIor?: number;\n /** Glass only: how much a fold over itself thickens the sheet. Glass draws opaque, so the layer\n * behind is otherwise invisible and a doubled-back ribbon looks exactly as thin as a single\n * sheet — this is the cue that reads as volume. 0 ignores overlap entirely. */\n glassLayerGain?: number;\n /** Glass only: DROPLET FUSION. Takes the refraction's direction from the gradient of the merged\n * silhouette rather than from each surface's own normal, so two sheets passing close read as one\n * blob of something viscous — in the neck between them the bend rotates smoothly from one rim to\n * the other instead of tearing between two centres. 0 is off and each sheet keeps its own. */\n glassFusion?: number;\n /** Glass only: CAUSTICS — brightness where the refraction map compresses and neighbouring rays\n * pile up, darkness where it spreads. Computed from the Jacobian of the sampling map, so it\n * costs no extra pass and lands exactly where the optics put it rather than being painted on.\n * Only visible where there is a backdrop to concentrate: a sheet over blank page has no light\n * to gather. */\n glassCaustic?: number;\n /** Glass only: LIQUID — how hard four travelling waves tilt the surface normal. Everything\n * downstream (dispersion, rim, specular) reads the rippled normal, so the shimmer stays coherent\n * instead of sitting on top as a separate layer. 0 is still glass, just not moving. */\n glassRipple?: number;\n /** Glass only: waves per world unit. */\n glassRippleScale?: number;\n /** Glass only: how fast they travel, rad/s. */\n glassFlow?: number;\n /** Glass only: falloff exponent of the rim band. Low = the whole sheet bends; high = only the\n * silhouette does, which is the crisp compression ring. */\n glassRimPower?: number;\n lineAmount?: number;\n lineThickness?: number;\n lineDerivativePower?: number;\n /** Wireframe only: how hard the strands recede INTO the page background with depth. 1 (the\n * default) is the original hardcoded fade — it gives a single ribbon its sense of depth, but on a\n * tightly-fitted near/far slab it washes out the whole back half of a deep or stacked\n * composition. 0 turns it off, so every strand holds full contrast wherever it sits: the flat,\n * graphic, poster look. */\n lineDepthFade?: number;\n /** Wireframe only: 0..1, how HARD the edge of each strand is. The stripe is a soft ramp by\n * default (0), which means {@link lineThickness} widens the strands by fading the gaps away with\n * them — the surface goes from pale hairlines to flat solid without passing through dense ink.\n * Raising this steepens the ramp about its midpoint, which splits the two controls apart:\n * `lineThickness` becomes the DUTY CYCLE (how much of each period is strand rather than gap) and\n * this becomes the edge. 0.9 with `lineThickness` ~1.5 is heavy ink with crisp gaps still\n * reading — the engraved / guilloché look. Default 0 (the original soft ramp). */\n lineSharpness?: number;\n /** Wireframe only: what sits between the strands. ABSENT = the page background, which makes the\n * wave a window onto the page (dark strands, paper showing through) — the theme as it has always\n * drawn. A colour makes the ribbon its own BODY: a dark gap colour under a bright palette is an\n * opaque striped surface, which is the other half of this theme's range and the one that reads as\n * a lit solid rather than a drawing. `\"transparent\"` (or an 8-digit hex with a low alpha) leaves\n * the gaps CLEAR instead, so stacked folds show through each other rather than occluding —\n * airier, but the near fold no longer hides the far one, which is what makes a stack read solid. */\n lineGapColor?: string;\n /** Wireframe only: 0..1, how much the strands are LIT. The line theme is otherwise unlit — a\n * strand's colour comes from its uv alone, so it holds one tone wherever the surface turns, which\n * is why a dense wireframe reads as a printed pattern instead of an object. Turn this up and the\n * same derivative normal and scene `lights` the solid theme uses shade it, AND each strand is\n * given a round cross-section, so a specular runs along one strand and not its neighbour. (Those\n * are one knob on purpose: shading a flat stripe barely reads — it is the round section that makes\n * a strand look like a filament.) Default 0. */\n lineLight?: 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 // 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 /** Optional per-wave particle / dust field emitted off THIS wave's deformed surface / edge.\n * ABSENT ⇒ off (no THREE.Points for this wave, byte-identical). See {@link ParticlesConfig}. */\n particles?: ParticlesConfig;\n /** Optional disintegration (\"the snap\"): a front that sweeps across the ribbon eating it away in\n * chunks, and — with a particle field — blowing those chunks off as dust. ABSENT ⇒ intact (no\n * DISSOLVE program, byte-identical). See {@link DissolveConfig}. */\n dissolve?: DissolveConfig;\n}\n\n/** Which way a {@link DissolveConfig} front sweeps. `length` / `width` follow the RIBBON's own uv\n * axes, so the front travels with the sheet wherever the twist takes it; `screenX` / `screenY` are\n * a straight line on the CANVAS, so every wave in a stack crumbles against the same edge whatever\n * each one's orientation. */\nexport type DissolveAxis = \"length\" | \"width\" | \"screenX\" | \"screenY\";\nexport const DISSOLVE_AXES: readonly DissolveAxis[] = [\"length\", \"width\", \"screenX\", \"screenY\"];\n\n/**\n * DISINTEGRATION — the \"snap\". A band sweeps across the ribbon in uv and everything behind it is\n * eaten away chunk by chunk, so the surface CRUMBLES rather than fading: holes open in it, the holes\n * merge, and the last fragments break off. Where the wave also has {@link ParticlesConfig}, `dust`\n * pins that field to the same front, so the motes are the chunks that just left — the surface does\n * not fade into an unrelated cloud, it becomes one.\n *\n * `amount` is the whole animation: 0 is intact and 1 is gone, whatever the band width, so binding it\n * to `scroll` (or any other input — it is a {@link WaveInteractionTarget}) disintegrates the wave as\n * the reader moves. Absent ⇒ the DISSOLVE shader path is never compiled.\n */\nexport interface DissolveConfig {\n /** 0..1 — how far the front has swept. 0 = the ribbon is whole; 1 = every chunk is gone. */\n amount: number;\n /** Which way the front travels. `\"length\"` (uv.y, end to end — the default) and `\"width\"` (uv.x,\n * across the folded cross-section) ride the ribbon, so the front bends with it; mind the axes —\n * uv.y is the LENGTH (see the UV AXES note atop WaveGeometry). `\"screenX\"` / `\"screenY\"` sweep a\n * straight line across the CANVAS instead, which is what makes a multi-wave composition crumble\n * as ONE object: give every wave the same axis and amount and they share one edge. The crumb\n * pattern stays on the surface either way. */\n axis?: DissolveAxis;\n /** Sweep from the far end instead of the near one. */\n reverse?: boolean;\n /** Width of the crumbling band, in uv. 0.05 is a clean guillotine edge; 0.6 is a long ragged fray\n * where half the ribbon is mid-flight at once. Default 0.35. */\n band?: number;\n /** How finely the ribbon is diced — chunks across its WIDTH (they are kept square on the sheet,\n * so the length gets ~2.1× as many). Default 90; smaller = big slabs, larger = fine grit. */\n scale?: number;\n /** 0..1 — chunk character: 0 = organic torn tatters (smooth noise), 1 = hard quantized cells\n * (blocky pixel debris). Default 0.6. */\n blocky?: number;\n /** 0..1 — how strongly this wave's own dust is pinned to the front: 1 = each mote peels off\n * exactly where and when the surface under it crumbles and drifts on from there; 0 = the field\n * free-runs on `life` as it always has. Default 1. Inert without {@link WaveConfig.particles}. */\n dust?: number;\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 \"tiltX\",\n \"tiltY\",\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 | \"tiltX\" // device tilt, the way a ball would roll: 1 = right edge down. 0.5 = neutral pose\n | \"tiltY\" // device tilt, the way a ball would roll: 1 = bottom edge down. 0.5 = neutral pose\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 \"dissolveAmount\",\n \"glassRipple\",\n \"glassStrength\",\n \"glassTint\",\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 /** Tuning for the device-tilt input. OPTIONAL even on a scene that uses tilt: a `tiltX` / `tiltY`\n * binding is what arms the sensor, and this only shapes how it reads. */\n tilt?: TiltConfig;\n /** Input→param bindings driving SCENE params (timeOffset, cameraZoom, blur, grain). */\n bindings?: SceneInteractionBinding[];\n}\n\n/**\n * Device tilt (a phone or tablet's orientation sensor) as an interaction input, feeding the `tiltX`\n * and `tiltY` sources. It is the one input a phone has that a cursor-authored scene doesn't: a page\n * whose bindings all read `pointerX` / `pointerY` is completely inert on a phone, and a couple of\n * tilt bindings — or `pointer: true` below — is what brings it back.\n *\n * Both axes read the way a ball would roll on the screen: `tiltX` → 1 as the right edge drops,\n * `tiltY` → 1 as the bottom edge drops, and both rest at 0.5 in whatever pose the reader was\n * already holding the device in when the first reading landed.\n *\n * This block is TUNING, not the on-switch: binding anything to `tiltX` / `tiltY` arms the sensor by\n * itself, the same way `pointerX` needs no \"pointer\" block. A scene that binds neither (and doesn't\n * set `pointer` below) attaches no `deviceorientation` listener at all.\n *\n * ARMED is not the same as LIVE, and on iOS it never becomes live on its own. Safari gates the\n * sensor behind a modal permission dialog, and nothing here opens one: a tilt-bound scene on an\n * iPhone reads 0.5 on both axes and renders exactly as it would with no tilt block at all. That is\n * deliberate — a background effect is not worth interrupting a reader to ask for a sensor. Design\n * for tilt as an enhancement that some phones simply don't get, the way you would a hover state.\n *\n * `renderer.enableTilt()` (or the shell handle's / the element's) is the explicit opt-in for a page\n * that has decided otherwise — an interactive piece a reader came to play with, where a tap that\n * opens the dialog is part of the deal. `renderer.tiltStatus()` reports `\"prompt\"` on a gated\n * platform, which is information, not an instruction to build a permission button.\n */\nexport interface TiltConfig {\n /** Degrees away from the neutral pose that reach the 0 / 1 ends. Default 25 — about the range of\n * a wrist, not of a whole arm. Smaller = a twitchier scene that reacts to a nudge. */\n range?: number;\n /** Follow smoothing, seconds. Default 0.18: sensor data is noisy and a still hand still jitters,\n * so this is slower than the pointer's 0.12. */\n smoothing?: number;\n /** Flip the horizontal / vertical direction (which way is \"up\" is a property of the scene). */\n invertX?: boolean;\n invertY?: boolean;\n /**\n * Also drive the shared CURSOR from tilt, so a scene authored for `pointerX` / `pointerY` — and\n * every wave's hover field — comes alive on a phone that has no cursor at all, without authoring\n * a second set of bindings. A real pointer always wins: this only fills in while none is on the\n * element. Default false.\n */\n pointer?: boolean;\n}\n\n/**\n * A WAVE's field of additive GPU sprites (dust / sparkle). Lives on {@link WaveConfig} — particles\n * belong to a wave, not the scene. ABSENT ⇒ off: no THREE.Points node is created for that wave and its\n * render is byte-identical; present ⇒ {@link normalizeParticles} clamps it. Every particle's motion is a\n * pure function of `uTime` + a per-particle seed baked from `seed`, so it is deterministic (timeOffset\n * scrub / loopSeconds / paused all hold). Particles spawn on the OWNING wave's DEFORMED surface / edge\n * (via the shared waveShape) and drift outward from the wave centre.\n */\n/** How each particle sprite is drawn (a per-field render style, not per-particle). All but\n * \"sprite\" are drawn procedurally from `gl_PointCoord`; \"sprite\" samples {@link\n * ParticlesConfig.spriteUrl} and falls back to \"glitter\" until that image has rasterized. */\nexport type ParticleShape = \"glitter\" | \"soft\" | \"ring\" | \"star\" | \"streak\" | \"square\" | \"sprite\";\nexport const PARTICLE_SHAPES: readonly ParticleShape[] = [\n \"glitter\",\n \"soft\",\n \"ring\",\n \"star\",\n \"streak\",\n \"square\",\n \"sprite\",\n];\n\nexport interface ParticlesConfig {\n count: number; // total sprites (clamped in normalizeParticles)\n size: number; // base sprite size, px\n seed: number; // PRNG seed → reproducible layout\n sizeJitter?: number; // 0..1 per-particle size variance\n color?: string; // sprite colour (warm gold default)\n /** Second sprite colour — particles interpolate between `color` and this by their seed (two-tone dust). */\n color2?: string;\n life?: number; // seconds per birth→death cycle\n /** Motion-speed multiplier for the dust: how fast particles cycle + drift (1 = default, 0 = frozen).\n * Independent of the wave's own `speed` (which animates the surface the dust rides). Under a seamless\n * `loopSeconds` it snaps to a whole number of cycles so the loop stays seamless. */\n speed?: number;\n twinkle?: number; // 0..1 brightness flicker\n /** Where on the wave particles spawn: 0 = across the whole SURFACE, 1 = the outer rim / EDGE only. */\n edgeBias?: number;\n /** How far particles drift outward from the wave centre as they age (world units). */\n drift?: number;\n /** −1..1 skew of the spawn along the edge width toward one flank (0 = even, −1 → one side, +1 → other). */\n bias?: number;\n /** Screen-vertical buoyancy over a lifetime, world units: + rises (embers), − falls (snow / ash). */\n rise?: number;\n /** Orbit around the wave centre in the screen plane, turns per lifetime (swirls the dust). */\n swirl?: number;\n /** Curl-noise turbulence: particles meander (fireflies / motes) instead of moving in straight lines. */\n wander?: number;\n /** Sprite render style. Default \"glitter\" (the soft round additive disc). */\n shape?: ParticleShape;\n /**\n * How the sprites composite. `\"additive\"` (the default) ADDS light — glints, embers, sparks; it can\n * only ever brighten, so additive dust is invisible on a white page and can never read as dark.\n * `\"normal\"` alpha-blends them instead, which is what a dark mote on a light ground needs: soot,\n * ash, ink, the blocky debris a {@link DissolveConfig} sheds across a pale background. Either way\n * the field never writes depth, so it composites over the waves rather than occluding them.\n */\n blend?: \"additive\" | \"normal\";\n /**\n * Artwork for `shape: \"sprite\"` — an SVG (or raster) `data:` URI or URL, rasterized ONCE into a\n * square texture shared by every particle in the field, so the cost is one texture per field and\n * not per particle. Ignored unless `shape` is \"sprite\"; until it has loaded the field draws\n * \"glitter\", so a slow or broken image degrades instead of blanking.\n *\n * The sprite is TINTED by `color` / `color2` (multiplied), so the field's colour knobs keep\n * working — supply WHITE artwork to take the tint literally, or coloured artwork to modulate it.\n * Non-square art is letterboxed, because a point sprite is always square.\n *\n * Prefer SVG: it is a fraction of the bytes of an equivalent PNG, and the whole config (this\n * string included) is embedded in save-states and share links.\n */\n spriteUrl?: string;\n /**\n * How hard the cursor shoves dust that has already drifted OFF the surface, as a multiple of the\n * displacement the wave's own {@link WaveHoverConfig} field applies. Default 1; 0 = airborne motes\n * ignore the pointer. Only ever reads when the owning wave has a pointer field of its own — with\n * no `interaction.hover` there is nothing to shove with, and the point program compiles without\n * the pointer path at all.\n *\n * Dust still ATTACHED to the surface is not governed by this: a mote sitting on the ribbon always\n * takes the ribbon's own displacement (weighted by how far it has drifted), because otherwise a\n * cursor poke would lift the surface out from under its own glitter.\n */\n pointerShove?: number;\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 // Radial off: amount 0 leaves the RADIAL block uncompiled (see waveDefines).\n radialAmount: 0,\n radialArc: 160,\n radialSpread: 1,\n radialRadius: 40,\n radialCenter: 0,\n radialCone: 0,\n radialSwirl: 0,\n theme: \"solid\",\n lineAmount: 425, // wireframe-theme line params (defaults)\n lineThickness: 1,\n lineDerivativePower: 0.95,\n lineDepthFade: 1,\n lineSharpness: 0,\n lineLight: 0,\n rungAmount: 0, // cross-wise rungs off\n rungThickness: 1,\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 // particles is deliberately absent (off = byte-identical).\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 (!Number.isFinite(s.radialAmount)) s.radialAmount = 0;\n if (!Number.isFinite(s.radialArc)) s.radialArc = 160;\n if (!Number.isFinite(s.radialSpread)) s.radialSpread = 1;\n if (!Number.isFinite(s.radialRadius)) s.radialRadius = 40;\n if (!Number.isFinite(s.radialCenter)) s.radialCenter = 0;\n if (!Number.isFinite(s.radialCone)) s.radialCone = 0;\n if (!Number.isFinite(s.radialSwirl)) s.radialSwirl = 0;\n if (typeof s.theme !== \"string\") s.theme = \"solid\";\n // Glass knobs are PRESENT-ONLY: a config without them is untouched, and the defaults below only\n // apply once a wave opts into the theme.\n if (s.theme === \"glass\") {\n if (!Number.isFinite(s.glassStrength)) s.glassStrength = 90;\n if (!Number.isFinite(s.glassChroma)) s.glassChroma = 0.7;\n if (!Number.isFinite(s.glassFrost)) s.glassFrost = 0.08;\n if (!Number.isFinite(s.glassSpec)) s.glassSpec = 1.2;\n if (!Number.isFinite(s.glassVibrancy)) s.glassVibrancy = 0.05;\n if (!Number.isFinite(s.glassTint)) s.glassTint = 0.12;\n if (!Number.isFinite(s.glassRimPower)) s.glassRimPower = 1.2;\n if (!Number.isFinite(s.glassRipple)) s.glassRipple = 0;\n if (!Number.isFinite(s.glassRippleScale)) s.glassRippleScale = 0.012;\n if (!Number.isFinite(s.glassFlow)) s.glassFlow = 0.9;\n if (!Number.isFinite(s.glassPath)) s.glassPath = 0.45;\n if (!Number.isFinite(s.glassDensity)) s.glassDensity = 1.2;\n if (!Number.isFinite(s.glassRim)) s.glassRim = 0.5;\n if (!Number.isFinite(s.glassIrid)) s.glassIrid = 0;\n if (!Number.isFinite(s.glassFilmNm)) s.glassFilmNm = 380;\n if (!Number.isFinite(s.glassIor)) s.glassIor = 1.45;\n if (!Number.isFinite(s.glassLayerGain)) s.glassLayerGain = 0.6;\n if (!Number.isFinite(s.glassFusion)) s.glassFusion = 0;\n if (!Number.isFinite(s.glassCaustic)) s.glassCaustic = 0.4;\n }\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.lineDepthFade)) s.lineDepthFade = 1;\n if (!Number.isFinite(s.lineSharpness)) s.lineSharpness = 0;\n // Absent is meaningful (= the page background), so this is repaired only when present and wrong.\n if (s.lineGapColor !== undefined && typeof s.lineGapColor !== \"string\") delete s.lineGapColor;\n if (!Number.isFinite(s.lineLight)) s.lineLight = 0;\n if (!Number.isFinite(s.rungAmount)) s.rungAmount = 0;\n if (!Number.isFinite(s.rungThickness)) s.rungThickness = 1;\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 if (s.particles) normalizeParticles(s); // present-only; absence = no field for this wave\n if (s.dissolve) normalizeDissolve(s); // present-only; absence = the ribbon is intact\n // Present-only, like `path`: an empty or blank name is no name, not an empty title.\n if (typeof s.name === \"string\") {\n const named = s.name.trim().slice(0, 60);\n if (named) s.name = named;\n else delete s.name;\n } else if (s.name !== undefined) delete s.name;\n if (s.path) normalizePath(s); // present-only; absence = the straight centreline\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 // particles is present-only (like interaction): NOT backfilled here — absence is off.\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.tilt) {\n const t = it.tilt;\n if (t.range !== undefined) t.range = clampNumber(t.range, 1, 90, 25);\n if (t.smoothing !== undefined) t.smoothing = clampNumber(t.smoothing, 0, 2, 0.18);\n }\n if (it.bindings !== undefined) {\n it.bindings = cleanBindings<SceneInteractionTarget>(it.bindings, SCENE_TARGET_NAMES);\n }\n}\n\n/**\n * Present-only normalizer for the scene PARTICLES block: clamp the numerics that are present and\n * repair the required fields, leaving absent optionals absent (so the block stays lean). NEVER call\n * when the block is absent — absence is off and byte-identical (ensureStudioConfig gates on presence).\n */\nexport function normalizeParticles(wave: WaveConfig): void {\n const p = wave.particles;\n if (!p) return;\n p.count = clampNumber(p.count, 0, 40000, 0);\n p.size = clampNumber(p.size, 0, 200, 2);\n p.seed = num(p.seed, 0);\n if (p.sizeJitter !== undefined) p.sizeJitter = clampNumber(p.sizeJitter, 0, 1, 0);\n if (p.color !== undefined && typeof p.color !== \"string\") p.color = \"#ffcf8a\";\n if (p.life !== undefined) p.life = clampNumber(p.life, 0.1, 60, 6);\n if (p.speed !== undefined) p.speed = clampNumber(p.speed, 0, 8, 1);\n if (p.twinkle !== undefined) p.twinkle = clampNumber(p.twinkle, 0, 1, 0);\n if (p.color2 !== undefined && typeof p.color2 !== \"string\") p.color2 = \"#ffcf8a\";\n if (p.edgeBias !== undefined) p.edgeBias = clampNumber(p.edgeBias, 0, 1, 1);\n if (p.drift !== undefined) p.drift = num(p.drift, 0);\n if (p.bias !== undefined) p.bias = clampNumber(p.bias, -1, 1, 0);\n if (p.rise !== undefined) p.rise = num(p.rise, 0);\n if (p.swirl !== undefined) p.swirl = num(p.swirl, 0);\n if (p.wander !== undefined) p.wander = num(p.wander, 0);\n if (p.shape !== undefined && !PARTICLE_SHAPES.includes(p.shape)) p.shape = \"glitter\";\n if (p.blend !== undefined && p.blend !== \"additive\" && p.blend !== \"normal\") p.blend = \"additive\";\n // Untrusted configs (share links / imported JSON) reach here — keep the url a string, but do not\n // validate the scheme: the renderer only ever hands it to an <img>, which sandboxes SVG scripts.\n if (p.spriteUrl !== undefined && typeof p.spriteUrl !== \"string\") delete p.spriteUrl;\n if (p.pointerShove !== undefined) p.pointerShove = clampNumber(p.pointerShove, 0, 4, 1);\n}\n\n/** Clamp a present {@link WaveConfig.path}: drop anything that is not a finite point, and drop the\n * whole path if fewer than two survive (one point is not a centreline). Present-only, like the\n * particle and dissolve blocks — absence means \"the straight ribbon\". */\nexport function normalizePath(wave: WaveConfig): void {\n const p = wave.path;\n if (!p) return;\n if (!Array.isArray(p)) {\n delete wave.path;\n return;\n }\n const pts = p\n .filter((q): q is PathPoint => !!q && typeof q === \"object\")\n .map((q) => ({\n x: num(q.x, 0),\n y: num(q.y, 0),\n z: num(q.z, 0),\n width: q.width === undefined ? undefined : clampNumber(q.width, 0, 8, 1),\n twist: q.twist === undefined ? undefined : num(q.twist, 0),\n }))\n .filter((q) => Number.isFinite(q.x) && Number.isFinite(q.y) && Number.isFinite(q.z));\n if (pts.length < 2) delete wave.path;\n else wave.path = pts;\n}\n\n/** Clamp a present {@link DissolveConfig} (present-only, like {@link normalizeParticles}: a wave\n * with no `dissolve` block is left exactly as it is). */\nexport function normalizeDissolve(wave: WaveConfig): void {\n const d = wave.dissolve;\n if (!d) return;\n d.amount = clampNumber(d.amount, 0, 1, 0);\n if (d.axis !== undefined && !DISSOLVE_AXES.includes(d.axis)) d.axis = \"length\";\n if (d.reverse !== undefined) d.reverse = !!d.reverse;\n if (d.band !== undefined) d.band = clampNumber(d.band, 0.01, 1, 0.35);\n if (d.scale !== undefined) d.scale = clampNumber(d.scale, 2, 600, 90);\n if (d.blocky !== undefined) d.blocky = clampNumber(d.blocky, 0, 1, 0.6);\n if (d.dust !== undefined) d.dust = clampNumber(d.dust, 0, 1, 1);\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 // each wave's normalizeWave runs normalizeWaveInteraction + normalizeParticles (both present-only)\n config.waves.forEach(normalizeWave);\n config.waveCount = config.waves.length;\n // Present-only: a config without a scene `interaction` block is left untouched (\"off\").\n if (config.interaction) normalizeSceneInteraction(config);\n return config;\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { BackgroundImageFit, BackgroundMode, BasicGradientType, BlendMode, CAMERA_FITS, CameraFit, ColorStop, DEFAULT_LIGHT_POSITION, GradientType, InteractionSource, LightConfig, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, MeshGradientPoint, NoiseBand, PARTICLE_SHAPES, PaletteSource, ParticleShape, ParticlesConfig, SceneConfig, SceneInteractionBinding, SceneInteractionConfig, SceneInteractionTarget, StudioConfig, TiltConfig, Vec2, Vec3, WaveConfig, WaveHoverConfig, WaveInteractionBinding, WaveInteractionConfig, WaveInteractionTarget, WavePressConfig, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, ensureCamera, ensureSceneDefaults, ensureStudioConfig, makeStops, makeWave, makeWaveSpread, normalizeBackground, normalizeParticles, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, resizeWaves } from "./config/model.js";
|
|
1
|
+
import { BackgroundImageFit, BackgroundMode, BasicGradientType, BlendMode, CAMERA_FITS, CameraFit, ColorStop, DEFAULT_LIGHT_POSITION, DISSOLVE_AXES, DissolveAxis, DissolveConfig, GradientType, InteractionSource, LightConfig, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, MeshGradientPoint, NoiseBand, PARTICLE_SHAPES, PaletteSource, ParticleShape, ParticlesConfig, PathPoint, SceneConfig, SceneInteractionBinding, SceneInteractionConfig, SceneInteractionTarget, StudioConfig, TiltConfig, Vec2, Vec3, WaveConfig, WaveHoverConfig, WaveInteractionBinding, WaveInteractionConfig, WaveInteractionTarget, WavePressConfig, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, ensureCamera, ensureSceneDefaults, ensureStudioConfig, makeStops, makeWave, makeWaveSpread, normalizeBackground, normalizeDissolve, normalizeParticles, normalizePath, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, resizeWaves } from "./config/model.js";
|
|
2
2
|
import { TiltStatus } from "./renderer/tilt.js";
|
|
3
3
|
import { WaveRenderer, WaveRendererOptions } from "./renderer/WaveRenderer.js";
|
|
4
4
|
import { PosterFit } from "./shell/poster.js";
|
|
5
5
|
import { FallbackReason, SnapshotOptions, WaveHandle, WaveOptions, WaveState, createWave, mountWave } from "./shell/createWave.js";
|
|
6
|
-
|
|
6
|
+
import { hasWebGL, isSoftwareRenderer, probeWebGL } from "./shell/probe.js";
|
|
7
|
+
export { BackgroundImageFit, BackgroundMode, BasicGradientType, BlendMode, CAMERA_FITS, CameraFit, ColorStop, DEFAULT_LIGHT_POSITION, DISSOLVE_AXES, DissolveAxis, DissolveConfig, type FallbackReason, GradientType, InteractionSource, LightConfig, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, MeshGradientPoint, NoiseBand, PARTICLE_SHAPES, PaletteSource, ParticleShape, ParticlesConfig, PathPoint, type PosterFit, SceneConfig, SceneInteractionBinding, SceneInteractionConfig, SceneInteractionTarget, type SnapshotOptions, StudioConfig, TiltConfig, type TiltStatus, Vec2, Vec3, WaveConfig, type WaveHandle, WaveHoverConfig, WaveInteractionBinding, WaveInteractionConfig, WaveInteractionTarget, type WaveOptions, WavePressConfig, type WaveRenderer, type WaveRendererOptions, type WaveState, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, createWave, ensureCamera, ensureSceneDefaults, ensureStudioConfig, hasWebGL, isSoftwareRenderer, makeStops, makeWave, makeWaveSpread, mountWave, normalizeBackground, normalizeDissolve, normalizeParticles, normalizePath, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, probeWebGL, resizeWaves };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { CAMERA_FITS, DEFAULT_LIGHT_POSITION, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, PARTICLE_SHAPES, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, ensureCamera, ensureSceneDefaults, ensureStudioConfig, makeStops, makeWave, makeWaveSpread, normalizeBackground, normalizeParticles, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, resizeWaves } from "./config/model.js";
|
|
1
|
+
import { CAMERA_FITS, DEFAULT_LIGHT_POSITION, DISSOLVE_AXES, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, PARTICLE_SHAPES, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, ensureCamera, ensureSceneDefaults, ensureStudioConfig, makeStops, makeWave, makeWaveSpread, normalizeBackground, normalizeDissolve, normalizeParticles, normalizePath, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, resizeWaves } from "./config/model.js";
|
|
2
|
+
import { hasWebGL, isSoftwareRenderer, probeWebGL } from "./shell/probe.js";
|
|
2
3
|
import { createWave, mountWave } from "./shell/createWave.js";
|
|
3
|
-
export { CAMERA_FITS, DEFAULT_LIGHT_POSITION, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, PARTICLE_SHAPES, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, createWave, ensureCamera, ensureSceneDefaults, ensureStudioConfig, makeStops, makeWave, makeWaveSpread, mountWave, normalizeBackground, normalizeParticles, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, resizeWaves };
|
|
4
|
+
export { CAMERA_FITS, DEFAULT_LIGHT_POSITION, DISSOLVE_AXES, MAX_COLORS, MAX_LIGHTS, MAX_MESH_POINTS, MAX_NOISE_BANDS, MAX_WAVES, PARTICLE_SHAPES, createDefaultConfig, createDefaultMeshPoints, createLight, createNoiseBand, createWave, ensureCamera, ensureSceneDefaults, ensureStudioConfig, hasWebGL, isSoftwareRenderer, makeStops, makeWave, makeWaveSpread, mountWave, normalizeBackground, normalizeDissolve, normalizeParticles, normalizePath, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, probeWebGL, resizeWaves };
|