@wave3d/core 0.7.0 → 0.9.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.
Files changed (75) hide show
  1. package/README.md +3 -1
  2. package/dist/config/model.d.ts +78 -4
  3. package/dist/config/model.js +12 -2
  4. package/dist/config/model.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/presets.js +35 -0
  7. package/dist/presets.js.map +1 -1
  8. package/dist/renderer/WaveRenderer.d.ts +146 -12
  9. package/dist/renderer/WaveRenderer.js +218 -56
  10. package/dist/renderer/WaveRenderer.js.map +1 -1
  11. package/dist/renderer/WaveRendererGPU.js +291 -0
  12. package/dist/renderer/WaveRendererGPU.js.map +1 -0
  13. package/dist/renderer/gpu-loader.js +2 -0
  14. package/dist/renderer/index.d.ts +3 -2
  15. package/dist/renderer/interaction.d.ts +29 -0
  16. package/dist/renderer/interaction.js +77 -31
  17. package/dist/renderer/interaction.js.map +1 -1
  18. package/dist/renderer/interactionGates.js +59 -0
  19. package/dist/renderer/interactionGates.js.map +1 -0
  20. package/dist/renderer/particleField.d.ts +1 -28
  21. package/dist/renderer/particleField.js +173 -16
  22. package/dist/renderer/particleField.js.map +1 -1
  23. package/dist/renderer/particleFieldGPU.js +141 -0
  24. package/dist/renderer/particleFieldGPU.js.map +1 -0
  25. package/dist/renderer/shaders.js +178 -86
  26. package/dist/renderer/shaders.js.map +1 -1
  27. package/dist/renderer/tilt.d.ts +17 -0
  28. package/dist/renderer/tilt.js +124 -0
  29. package/dist/renderer/tilt.js.map +1 -0
  30. package/dist/renderer/tsl/color.js +129 -0
  31. package/dist/renderer/tsl/color.js.map +1 -0
  32. package/dist/renderer/tsl/noise.js +83 -0
  33. package/dist/renderer/tsl/noise.js.map +1 -0
  34. package/dist/renderer/tsl/packedArray.js +76 -0
  35. package/dist/renderer/tsl/packedArray.js.map +1 -0
  36. package/dist/renderer/tsl/particleMaterial.js +133 -0
  37. package/dist/renderer/tsl/particleMaterial.js.map +1 -0
  38. package/dist/renderer/tsl/particleUniforms.js +44 -0
  39. package/dist/renderer/tsl/particleUniforms.js.map +1 -0
  40. package/dist/renderer/tsl/pointerField.js +73 -0
  41. package/dist/renderer/tsl/pointerField.js.map +1 -0
  42. package/dist/renderer/tsl/post.js +63 -0
  43. package/dist/renderer/tsl/post.js.map +1 -0
  44. package/dist/renderer/tsl/postChain.js +56 -0
  45. package/dist/renderer/tsl/postChain.js.map +1 -0
  46. package/dist/renderer/tsl/postEffects.js +221 -0
  47. package/dist/renderer/tsl/postEffects.js.map +1 -0
  48. package/dist/renderer/tsl/types.js +28 -0
  49. package/dist/renderer/tsl/types.js.map +1 -0
  50. package/dist/renderer/tsl/uniforms.js +152 -0
  51. package/dist/renderer/tsl/uniforms.js.map +1 -0
  52. package/dist/renderer/tsl/waveMaterial.js +185 -0
  53. package/dist/renderer/tsl/waveMaterial.js.map +1 -0
  54. package/dist/renderer/tsl/waveShape.js +108 -0
  55. package/dist/renderer/tsl/waveShape.js.map +1 -0
  56. package/dist/shell/createWave.d.ts +29 -0
  57. package/dist/shell/createWave.js +39 -11
  58. package/dist/shell/createWave.js.map +1 -1
  59. package/dist/shell/probe.js +18 -1
  60. package/dist/shell/probe.js.map +1 -1
  61. package/dist/standalone/wave3d.standalone.js +2219 -1929
  62. package/dist/standalone/wave3d.standalone.webgpu.js +36647 -0
  63. package/dist/standalone.d.ts +10 -3
  64. package/dist/standalone.js +10 -3
  65. package/dist/standalone.js.map +1 -1
  66. package/dist/studio/StudioWaveRenderer.d.ts +4 -0
  67. package/dist/studio/StudioWaveRenderer.js +9 -0
  68. package/dist/studio/StudioWaveRenderer.js.map +1 -1
  69. package/dist/studio/StudioWaveRendererGPU.js +18 -0
  70. package/dist/studio/StudioWaveRendererGPU.js.map +1 -0
  71. package/dist/studio/index.d.ts +12 -2
  72. package/dist/studio/index.js +14 -1
  73. package/dist/studio/index.js.map +1 -0
  74. package/package.json +12 -3
  75. package/skills/wave3d/SKILL.md +29 -2
package/dist/presets.js CHANGED
@@ -98,6 +98,10 @@ const PARTICLE_PRESETS = {
98
98
  };
99
99
  /** Degrees → radians (for the Particle Zoo's per-wave rotation, authored in degrees). */
100
100
  const rad = (deg) => deg * Math.PI / 180;
101
+ /** Identity, but it pins a Zoo entry's inline interaction literal to {@link WaveInteractionConfig}:
102
+ * without it the entries are merely INFERRED, so `source` / `target` widen to `string` (needing an
103
+ * `as const` per field) and a misspelt knob would slip through unchecked. */
104
+ const interaction = (it) => it;
101
105
  /** Clamp a signed magnitude to ±m (used to cap the Zoo's long-range particle motion terms). */
102
106
  const capMag = (v, m) => v == null ? v : Math.sign(v) * Math.min(Math.abs(v), m);
103
107
  /** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and
@@ -582,9 +586,20 @@ const PRESETS = {
582
586
  z: 0
583
587
  };
584
588
  c.cameraZoom = .6;
589
+ c.interaction = {
590
+ enabled: true,
591
+ radius: .22,
592
+ ribbonFlow: .5,
593
+ touch: true
594
+ };
585
595
  const zoo = [
586
596
  {
587
597
  style: "Embers",
598
+ interact: interaction({ hover: {
599
+ agitate: 34,
600
+ lighten: .25
601
+ } }),
602
+ shove: 2.5,
588
603
  palette: [
589
604
  "#ff7a1e",
590
605
  "#ffd27a",
@@ -617,6 +632,11 @@ const PRESETS = {
617
632
  },
618
633
  {
619
634
  style: "Snow",
635
+ interact: interaction({ hover: {
636
+ push: -22,
637
+ smoothing: .45
638
+ } }),
639
+ shove: .6,
620
640
  palette: [
621
641
  "#8fb8e8",
622
642
  "#e8f1fb",
@@ -649,6 +669,8 @@ const PRESETS = {
649
669
  },
650
670
  {
651
671
  style: "Sparks",
672
+ interact: interaction({ press: { ripple: 46 } }),
673
+ shove: 4,
652
674
  palette: [
653
675
  "#ffd27a",
654
676
  "#fff4d0",
@@ -677,6 +699,12 @@ const PRESETS = {
677
699
  },
678
700
  {
679
701
  style: "Fireflies",
702
+ interact: interaction({ bindings: [{
703
+ source: "hover",
704
+ target: "helixPhase",
705
+ to: 360,
706
+ smoothing: .5
707
+ }] }),
680
708
  palette: [
681
709
  "#3d5a24",
682
710
  "#7bd23a",
@@ -708,6 +736,11 @@ const PRESETS = {
708
736
  },
709
737
  {
710
738
  style: "Bubbles",
739
+ interact: interaction({ hover: {
740
+ wake: 30,
741
+ thin: .45
742
+ } }),
743
+ shove: 1.6,
711
744
  palette: [
712
745
  "#2e8fb0",
713
746
  "#7fd4e8",
@@ -780,7 +813,9 @@ const PRESETS = {
780
813
  if (p.rise != null) p.rise = capMag(p.rise * PULL, 28);
781
814
  if (p.wander != null) p.wander = capMag(p.wander * PULL, 15);
782
815
  if (p.swirl != null) p.swirl = capMag(p.swirl * PULL, .05);
816
+ if (z.shove != null) p.pointerShove = z.shove;
783
817
  w.particles = p;
818
+ if (z.interact) w.interaction = structuredClone(z.interact);
784
819
  return w;
785
820
  });
786
821
  c.waveCount = c.waves.length;
@@ -1 +1 @@
1
- {"version":3,"file":"presets.js","names":[],"sources":["../src/presets.ts"],"sourcesContent":["/**\n * Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.\n * IP-clean — no copyrighted assets. The studio layers its own extra presets (and its historical\n * \"Stripe *\" display names) on top; see apps/studio/src/presets.ts.\n */\nimport { createDefaultConfig, makeStops, makeWaveSpread } from \"./config/model\";\nimport type { ParticlesConfig, StudioConfig, NoiseBand } from \"./config/model\";\n\nconst RAD = 180 / Math.PI;\n\n/**\n * Reusable particle STYLES applied to a wave's `particles` block. Surfaced in the studio's per-wave\n * Particles folder (the \"style\" picker) and used by the \"Particle Zoo\" showcase preset. Each is a dust\n * look independent of the wave it rides — clone before mutating (`structuredClone`).\n */\nexport const PARTICLE_PRESETS: Record<string, ParticlesConfig> = {\n Glitter: {\n count: 16000,\n size: 2.6,\n seed: 7,\n color: \"#ffdca8\",\n shape: \"glitter\",\n edgeBias: 1,\n drift: 300,\n bias: -0.6,\n twinkle: 0.8,\n },\n Embers: {\n count: 16000,\n size: 3.6,\n seed: 7,\n color: \"#ff8a2c\",\n color2: \"#ffe19a\",\n shape: \"soft\",\n edgeBias: 0.15,\n drift: 40,\n rise: 320,\n wander: 120,\n twinkle: 0.85,\n life: 6,\n },\n Snow: {\n count: 12000,\n size: 3,\n seed: 4,\n color: \"#eef4ff\",\n color2: \"#bcd0ee\",\n shape: \"soft\",\n edgeBias: 0.1,\n drift: 20,\n rise: -260,\n wander: 80,\n twinkle: 0.5,\n life: 8,\n },\n Sparks: {\n count: 9000,\n size: 5,\n seed: 9,\n color: \"#ffd27a\",\n color2: \"#fff4d0\",\n shape: \"streak\",\n edgeBias: 1,\n drift: 620,\n rise: 90,\n wander: 20,\n bias: -0.4,\n twinkle: 0.9,\n life: 2.5,\n },\n Fireflies: {\n count: 2200,\n size: 5,\n seed: 11,\n color: \"#dfff9a\",\n color2: \"#8fd94a\",\n shape: \"star\",\n edgeBias: 0.1,\n drift: 25,\n swirl: 0.3,\n wander: 170,\n twinkle: 1,\n life: 5,\n },\n Bubbles: {\n count: 3000,\n size: 7,\n sizeJitter: 0.9,\n seed: 6,\n color: \"#cdeefb\",\n color2: \"#8fd0e6\",\n shape: \"ring\",\n edgeBias: 0.2,\n drift: 30,\n rise: 240,\n wander: 60,\n twinkle: 0.3,\n life: 7,\n },\n};\n\n/** Degrees → radians (for the Particle Zoo's per-wave rotation, authored in degrees). */\nconst rad = (deg: number): number => (deg * Math.PI) / 180;\n\n/** Clamp a signed magnitude to ±m (used to cap the Zoo's long-range particle motion terms). */\nconst capMag = (v: number | undefined, m: number): number | undefined =>\n v == null ? v : Math.sign(v) * Math.min(Math.abs(v), m);\n\n/** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and\n * converted to degrees. All presets are solid-theme, so they reuse the hero palette +\n * surfaceColor fibers (600/0.2) and sheen 0, like the hero. camTarget/zoom frame the\n * wave (we pan the look-at to centre each one). */\nfunction buildPreset(p: {\n speed: number;\n contrast: number;\n sat: number;\n hueRad: number;\n dispX: number;\n dispZ: number;\n dispAmt: number;\n pos: [number, number, number];\n rotRad: [number, number, number];\n scale: [number, number, number];\n twF: [number, number, number];\n twP: [number, number, number];\n glow: [number, number, number];\n grain: number;\n blur: number;\n zoom: number;\n camTarget: [number, number];\n noiseBands?: NoiseBand[];\n twistMotion?: boolean;\n}): StudioConfig {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.speed = p.speed;\n w.colorContrast = p.contrast;\n w.colorSaturation = p.sat;\n w.hueShift = p.hueRad * RAD;\n w.displaceFrequency = { x: p.dispX, y: p.dispZ };\n w.displaceAmount = p.dispAmt;\n w.position = { x: p.pos[0], y: p.pos[1], z: p.pos[2] };\n w.rotation = { x: p.rotRad[0] * RAD, y: p.rotRad[1] * RAD, z: p.rotRad[2] * RAD };\n w.scale = { x: p.scale[0], y: p.scale[1], z: p.scale[2] };\n w.twistFrequency = { x: p.twF[0], y: p.twF[1], z: p.twF[2] };\n w.twistPower = { x: p.twP[0], y: p.twP[1], z: p.twP[2] };\n w.creaseLight = p.glow[0];\n w.creaseSharpness = p.glow[1];\n w.creaseSoftness = p.glow[2];\n if (p.noiseBands) w.noiseBands = p.noiseBands;\n if (p.twistMotion) w.twistMotion = true;\n c.grain = p.grain;\n c.blur = p.blur;\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: p.camTarget[0], y: p.camTarget[1], z: 0 };\n c.cameraZoom = p.zoom;\n return c;\n}\n\n/** Presets: each a complete studio config (scene + one or more waves) in the wave model. */\nexport const PRESETS: Record<string, () => StudioConfig> = {\n // The app's default wave: a centred, full-frame ribbon (window-independent framing).\n // Shown first and named \"Hero\"; several presets below derive from it.\n Hero: () =>\n buildPreset({\n speed: 0.04,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [380, -301.7, -11.1],\n rotRad: [-0.44959, -0.11759, 1.874407],\n scale: [9, 8, 5],\n twF: [-0.65, 0.41, -0.58],\n twP: [3.63, 0.7, 3.95],\n glow: [1.98, 0.806, 0.834],\n grain: 1.1,\n blur: 0.02,\n zoom: 0.55,\n camTarget: [-420, -200], // user-tuned default framing\n }),\n // Stripe's real hero, recreated faithfully: an orthographic ×10 scene that overflows the\n // frame, so only the twisted crop shows. This is the model's plain default config.\n \"Wave 2\": () => createDefaultConfig(),\n // camTarget on the waves below is a first-pass centring; tune per-wave. NOTE: Wave 4 also\n // uses a variant vertex shader (animated twist-X wobble) we don't fully replicate — its\n // STATIC frame is close, the motion differs.\n \"Wave 3\": () =>\n buildPreset({\n speed: 0.08,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [-200.7, -65.4, -11.1],\n rotRad: [-2.875593, 3.095927, -2.925927],\n scale: [3, 3, 3],\n twF: [0.059, 0.32, -0.397],\n twP: [3.63, 0.44, 5.99],\n glow: [3.86, 0.923, 1],\n grain: 1.2,\n blur: 0.02,\n zoom: 1.3,\n camTarget: [-104, 13], // centred; zoomed in (wide/flat wave)\n }),\n \"Wave 4\": () =>\n buildPreset({\n speed: 0.0525,\n contrast: 0.969,\n sat: 1.383,\n hueRad: 0.0376991,\n dispX: 0.005,\n dispZ: 0.0212,\n dispAmt: 6.68,\n pos: [206.1, -438, -11.1],\n rotRad: [-0.666018, -0.031416, 0.779115],\n scale: [6.0501, 8.3983, 6.9854],\n twF: [-0.424, 0.024, -1.312],\n twP: [1.81, 0.94, 4.76],\n glow: [1.55, 1.174, 0.972],\n grain: 0.576,\n blur: 0,\n zoom: 0.9316,\n camTarget: [194, -402], // centred on the wave\n twistMotion: true, // variant vertex shader — animated twist-X wobble\n noiseBands: [\n {\n startX: 0.856,\n endX: 1,\n startY: 0,\n endY: 0.913,\n feather: 0.5,\n strength: 0.346,\n frequency: 1018,\n colorAttenuation: 1,\n parabolaPower: 0,\n },\n {\n startX: 0.038,\n endX: 0.538,\n startY: 0.105,\n endY: 1,\n feather: 0.3315,\n strength: 1,\n frequency: 190,\n colorAttenuation: 0,\n parabolaPower: 2.11,\n },\n ],\n }),\n // The dark-background hero: identical geometry/camera to the default hero, but theme\n // \"wireframe\" → the line shader on a dark page background, with grain 1.2. Same palette.\n Wireframe: () => {\n const c = createDefaultConfig();\n c.waves[0].theme = \"wireframe\";\n c.grain = 1.2;\n c.background = \"#0a2540\"; // dark navy page background\n c.transparentBackground = false;\n return c;\n },\n \"Neon Dark Multistrand\": () => {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.theme = \"wireframe\"; // line shader on the near-black background — neon wireframe look\n w.blendMode = \"additive\";\n w.palette = makeStops([\"#00f5d4\", \"#00bbf9\", \"#9b5de5\", \"#f15bb5\", \"#fee440\"]);\n w.creaseLight = 1.0;\n c.background = \"#05060c\";\n c.transparentBackground = false; // fill the dark bg so the neon lines read on black (not the page)\n c.waves = makeWaveSpread(w, 3); // three overlapping neon waves\n c.waveCount = 3;\n return c;\n },\n \"Mesh Gradient\": () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n c.grain = 0.3;\n c.blur = 0.008;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n \"Solar Bloom\": () => {\n // Radial gradient: a warm core blooming out to a deep-indigo edge. usePaletteTexture off so\n // our own stops map along the radial gradCoord instead of sampling the baked hero LUT.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"radial\";\n w.gradientShift = 0.14;\n w.palette = [\n { color: \"#fff3c4\", pos: 0 }, // warm-white core\n { color: \"#ffd166\", pos: 0.22 }, // gold\n { color: \"#ff8c42\", pos: 0.42 }, // orange\n { color: \"#ff5d8f\", pos: 0.62 }, // coral-pink\n { color: \"#a64dff\", pos: 0.82 }, // violet\n { color: \"#241246\", pos: 1 }, // deep indigo edge\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.01;\n // Deep warm radial vignette behind the bloom.\n c.background = \"#0a0714\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"radial\";\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#2a1330\", \"#08040f\"]);\n c.transparentBackground = false;\n return c;\n },\n \"Latte Ring\": () => {\n // A warm cream-and-gold ring of combed silk curling around a dark void — the crema swirl on a\n // latte. The camera ORBITS a wide radial fan (arc 286°) so its combed length sweeps across the\n // frame as a flowing arc instead of a head-on fan; a mesh gradient warms it gold→cream with an\n // orange edge, and a noise band frays the fibers finer toward the tips. Exercises the radial wave\n // mode + the particle layer (ambient field + shed-from-edge dust into the void).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n // A wide radial fan (fans the ribbon's length); the orbiting camera below crops it to a ring.\n w.radialAmount = 0.72;\n w.radialArc = 286;\n w.radialCenter = -160;\n w.radialRadius = 300;\n w.radialSpread = 1.47;\n w.rotation = { x: 0, y: 0, z: 0 };\n w.position = { x: -280, y: -320, z: 0 };\n w.scale = { x: 1.12, y: 1.12, z: 1.12 };\n w.opacity = 0.5;\n // A big broad swell (amount 101.73) with a fine second octave riding on it, so the ring's silk\n // ripples and folds as it turns; `speed` sets the swell's drift rate.\n w.displaceFrequency = { x: 0.0026, y: 0.0048 };\n w.displaceAmount = 101.73;\n w.detailAmount = 6;\n w.detailFrequency = 0.1;\n w.speed = 0.21;\n // Fine combed fibers; the noise band overrides them finer + wispier over the OUTER half (uv.y>0.5)\n // for organic variation instead of a uniform comb. (Bands override fiber params per uv region.)\n w.fiberCount = 110;\n w.fiberStrength = 0.95;\n w.creaseLight = 0.55;\n w.edgeFeather = 0.34;\n w.noiseBands = [\n {\n startX: 0,\n endX: 1,\n startY: 0.5,\n endY: 1,\n feather: 0.4,\n strength: 1,\n frequency: 300,\n colorAttenuation: 0.85,\n parabolaPower: 2.5,\n },\n ];\n // Mesh gradient: gold + cream dominant with an orange accent — the latte's crema tones. A 2D\n // colour field (not a length-wise ramp) so the warmth pools within the fibers.\n w.usePaletteTexture = false;\n w.gradientType = \"mesh\";\n w.meshGradientSoftness = 0.7;\n w.meshGradientPoints = [\n { color: \"#f3c06a\", x: 0.5, y: 0.1, influence: 0.7 }, // warm gold\n { color: \"#f9edd4\", x: 0.34, y: 0.36, influence: 0.62 }, // hot cream\n { color: \"#e6923a\", x: 0.15, y: 0.55, influence: 0.5 }, // orange accent\n { color: \"#f1d49a\", x: 0.82, y: 0.42, influence: 0.55 }, // gold\n { color: \"#ece7d8\", x: 0.58, y: 0.88, influence: 0.72 }, // cool cream\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.15;\n // Orbit the camera around the fan so its length reads as a sweeping arc wrapping the void.\n c.cameraDistance = 600;\n c.cameraPosition = { x: -854.327, y: 135.331, z: 478.048 };\n c.cameraTarget = { x: -720.043, y: 10.575, z: -93.27 };\n c.cameraZoom = 1.222;\n // Scene: a near-black void with gentle bloom on the silk + dust.\n c.background = \"#050404\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.25;\n c.blur = 0;\n c.bloomStrength = 0.18;\n c.bloomRadius = 0.55;\n c.bloomThreshold = 0.72;\n // Golden dust shed off the ring's own edge into the void (biased to one flank). Per-wave: it rides\n // this wave's deform and drifts outward from it. edgeBias 1 = spawn on the rim (the shed look).\n w.particles = {\n count: 20000,\n size: 2.9,\n color: \"#ffdca8\",\n seed: 7,\n twinkle: 0.8,\n edgeBias: 1,\n drift: 490,\n bias: -0.6,\n };\n return c;\n },\n \"Particle Zoo\": () => {\n // One scene demoing every particle STYLE — each wave sheds a different kind of dust off its own\n // surface (embers / snow / sparks / fireflies / bubbles). Every wave is a DIFFERENT shape family\n // too (dome / flat sheet / radial fan / helix coil / twist curl), spread apart so none overlap and\n // the frame stays full. The showcase for the per-wave particle system; styles come from\n // PARTICLE_PRESETS (the studio \"preset style\" picker).\n const c = createDefaultConfig();\n c.background = \"#05070d\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.26;\n c.blur = 0;\n c.bloomStrength = 0.3;\n c.bloomThreshold = 0.66; // high, so the central sparks fan glows without blowing the middle out\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: 0, y: 0, z: 0 };\n c.cameraZoom = 0.6;\n // Five well-separated waves, each a DIFFERENT shape family so the silhouettes read as distinct,\n // not five copies of one ribbon. Frame at cameraZoom 0.6 spans ~2222×1250 world units centred on\n // the origin (x ∈ [-1111, 1111], y ∈ [-625, 625], orthographic → screen-xy = world-xy), so the\n // positions/scales below keep the meshes from overlapping. `shape` carries the per-family knobs\n // (displace / twist / helix / radial); `rot` is in degrees. style, palette, opacity as before.\n const zoo = [\n {\n // Embers rising off a broad DOME — low-frequency, high-amplitude displacement mound.\n style: \"Embers\",\n palette: [\"#ff7a1e\", \"#ffd27a\", \"#c23a12\"],\n pos: [-720, 285],\n scale: [1.0, 1.0, 1.0],\n rot: [-20, 0, -14],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.0024, y: 0.0065 },\n displaceAmount: 118,\n twistPower: { x: 2.4, y: 2.4, z: 3.2 },\n },\n },\n {\n // Snow settling on a near-flat drifting SHEET — tilted toward the camera, barely rippled.\n style: \"Snow\",\n palette: [\"#8fb8e8\", \"#e8f1fb\", \"#aeb9d6\"],\n pos: [700, 320],\n scale: [1.1, 1.0, 0.9],\n rot: [-62, 0, 12],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.006, y: 0.013 },\n displaceAmount: 22,\n twistPower: { x: 0.8, y: 0.8, z: 1.0 },\n },\n },\n {\n // Sparks bursting off a RADIAL FAN — the one spiky, fanned silhouette; dimmed so it glows\n // without blowing the centre out.\n style: \"Sparks\",\n palette: [\"#ffd27a\", \"#fff4d0\", \"#c8853a\"],\n pos: [0, -30],\n scale: [0.85, 0.85, 0.85],\n rot: [0, 0, 0],\n opacity: 0.32,\n shape: {\n radialAmount: 0.82,\n radialArc: 118,\n radialCenter: 90, // fan points up\n radialRadius: 40,\n radialSpread: 1.0,\n displaceAmount: 30,\n },\n },\n {\n // Fireflies wandering around a HELIX coil — a screw-thread column of light.\n style: \"Fireflies\",\n palette: [\"#3d5a24\", \"#7bd23a\", \"#40521f\"],\n pos: [-560, -300],\n scale: [1.25, 1.25, 1.25],\n rot: [0, 0, 8],\n opacity: 0.72,\n shape: {\n helixTurns: 3,\n helixRadius: 44,\n helixRoll: 1,\n displaceAmount: 8,\n twistPower: { x: 1.0, y: 1.0, z: 1.2 },\n },\n },\n {\n // Bubbles rising off a strongly TWISTED curl — an S-ribbon coiling on its length.\n style: \"Bubbles\",\n palette: [\"#2e8fb0\", \"#7fd4e8\", \"#1a5a6e\"],\n pos: [660, -270],\n scale: [1.0, 1.15, 1.0],\n rot: [-16, 0, -20],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.005, y: 0.011 },\n displaceAmount: 46,\n twistFrequency: { x: -0.06, y: 0.09, z: -0.5 },\n twistPower: { x: 4.0, y: 6.0, z: 9.0 },\n },\n },\n ];\n const base = c.waves[0];\n c.waves = zoo.map((z, i) => {\n const w = structuredClone(base);\n w.usePaletteTexture = false;\n w.gradientType = \"linear\";\n w.gradientAngle = 0;\n w.palette = makeStops(z.palette);\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1.1;\n w.opacity = z.opacity;\n w.scale = { x: z.scale[0], y: z.scale[1], z: z.scale[2] };\n w.rotation = { x: rad(z.rot[0]), y: rad(z.rot[1]), z: rad(z.rot[2]) };\n w.position = { x: z.pos[0], y: z.pos[1], z: 0 };\n w.seed = i * 7;\n w.speed = 0.05;\n Object.assign(w, z.shape); // the wave's distinct shape family (displace / twist / helix / radial)\n // Each PARTICLE_PRESET is tuned as a standalone hero cloud whose dust flings far; here every\n // specimen should sit ON the wave that emits it, reading as dust clinging to the surface rather\n // than a cloud floating nearby. Pull the motion terms right down (a small fraction of the hero\n // reach + tight clamps) so the dust overlaps its own wave; the styles still read apart by sprite\n // shape / colour / wave shape.\n const p = structuredClone(PARTICLE_PRESETS[z.style]);\n const PULL = 0.13; // fraction of each preset's hero-scale reach kept here\n if (p.drift != null) p.drift = capMag(p.drift * PULL, 24);\n if (p.rise != null) p.rise = capMag(p.rise * PULL, 28);\n if (p.wander != null) p.wander = capMag(p.wander * PULL, 15);\n if (p.swirl != null) p.swirl = capMag(p.swirl * PULL, 0.05);\n w.particles = p;\n return w;\n });\n c.waveCount = c.waves.length;\n return c;\n },\n Holographic: () => {\n // Conic gradient: an iridescent oil-slick sweep. The palette wraps (first ≈ last stop) so\n // the conic seam is invisible.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"conic\";\n w.gradientShift = 0.08;\n w.palette = [\n { color: \"#8ef6e4\", pos: 0 }, // mint (seam)\n { color: \"#6ec3ff\", pos: 0.18 }, // sky\n { color: \"#9b8cff\", pos: 0.36 }, // periwinkle\n { color: \"#ff8ad8\", pos: 0.54 }, // pink\n { color: \"#ffd98e\", pos: 0.72 }, // peach\n { color: \"#a0f0c8\", pos: 0.88 }, // seafoam\n { color: \"#8ef6e4\", pos: 1 }, // mint again (seamless wrap)\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.04;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.12;\n c.grain = 0.28;\n c.blur = 0.01;\n // Subtle deep teal → violet wash behind the iridescence.\n c.background = \"#05060c\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"linear\";\n c.backgroundGradientAngle = 135;\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#04121a\", \"#0a0518\"]);\n c.transparentBackground = false;\n return c;\n },\n Aurora: () => {\n // Mesh gradient: a moody aurora — teals/greens drifting into violet over a night-sky base\n // (distinct from the brighter iOS-style \"Mesh Gradient\").\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a1f3c\", x: 0.08, y: 0.12, influence: 0.62 },\n { color: \"#1fddb0\", x: 0.3, y: 0.7, influence: 0.78 },\n { color: \"#57f5a3\", x: 0.58, y: 0.86, influence: 0.7 },\n { color: \"#3a86ff\", x: 0.82, y: 0.55, influence: 0.62 },\n { color: \"#a15cff\", x: 0.5, y: 0.32, influence: 0.7 },\n { color: \"#071433\", x: 0.92, y: 0.08, influence: 0.6 },\n ];\n w.meshGradientSoftness = 0.72;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.008;\n // Dark night-sky MESH backdrop (also shows off the mesh background type).\n c.background = \"#03060f\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundMeshPoints = [\n { color: \"#02040c\", x: 0.15, y: 0.85, influence: 0.7 },\n { color: \"#08243a\", x: 0.5, y: 0.5, influence: 0.75 },\n { color: \"#0a0f2e\", x: 0.85, y: 0.7, influence: 0.7 },\n { color: \"#04121a\", x: 0.7, y: 0.2, influence: 0.6 },\n { color: \"#000208\", x: 0.12, y: 0.12, influence: 0.6 },\n ];\n c.backgroundMeshSoftness = 0.75;\n c.transparentBackground = false;\n return c;\n },\n Palestine: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"palestine\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1;\n c.grain = 0.35;\n c.background = \"#f2efe8\";\n c.transparentBackground = true;\n return c;\n },\n Spain: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"spain\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.18;\n w.colorSaturation = 1.25;\n w.creaseLight = 1.6; // moderate crease-light: rich crimson without washing to salmon (Hero's is 1.98)\n c.grain = 0.3;\n c.background = \"#1a0608\"; // deep oxblood stage\n c.backgroundMode = \"color\";\n c.transparentBackground = false; // opaque, so the dark stage makes the flag pop\n return c;\n },\n \"Vaporwave Sunset\": () => {\n // The Hero wave re-posed/re-framed, plus the vaporwave palette.\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.position.x = 525;\n w.rotation.x = -0.64 * RAD;\n w.rotation.z = 1.68 * RAD;\n w.paletteSource = \"vaporwave\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.08;\n w.colorSaturation = 1.15;\n w.creaseLight = 1.25;\n c.cameraZoom = 1.1;\n c.cameraTarget = { x: 150, y: 360, z: 0 };\n c.background = \"#09051f\";\n c.transparentBackground = false;\n return c;\n },\n Corkscrew: () => {\n // The helix mode, shown off on its own: `helixRoll` at 1 rolls the ribbon's cross-section in\n // step with the sweep, so the flat strip becomes an auger blade winding around its own length\n // axis, and `helixRadius` lifts that blade off the axis so the turns read as a screw thread\n // rather than a flat twist. No twist at all — this shape is unreachable with twistFrequency,\n // whose expStep angle is monotone and can only ramp once (see the helix docs in config/model).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.helixTurns = 5;\n w.helixRadius = 45;\n w.helixRoll = 1;\n w.helixPhase = 0;\n w.twistFrequency = { x: 0, y: 0, z: 0 };\n w.twistPower = { x: 4, y: 4, z: 2 };\n // A slow swell along the blade so it breathes; the corkscrew itself is static geometry.\n w.displaceAmount = 16;\n w.displaceFrequency = { x: 0.006, y: 0.0008 };\n w.speed = 0.1;\n w.position = { x: 0, y: 0, z: 0 };\n w.rotation = { x: 0, y: 0, z: 12 }; // tilt so it climbs across the frame\n w.scale = { x: 3, y: 3, z: 1.5 };\n // Mesh gradient: the colour field runs along the blade, so each turn picks up a different part\n // of the spectrum instead of the one hue a linear stop ramp would give.\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n // Framed down the axis rather than side-on: the coil reads as a screw receding into the frame,\n // and each turn shows its blade face instead of an edge. cameraDistance is the orbit radius\n // (= |position − target|); the camera is orthographic, so it's the rig's dolly, not the scale —\n // cameraZoom sets that.\n c.cameraPosition = { x: -526.009, y: -285.284, z: -425.489 };\n c.cameraTarget = { x: -95.046, y: -17.053, z: -105.608 };\n c.cameraDistance = 600;\n c.cameraZoom = 1.176;\n c.grain = 0.3;\n c.blur = 0.008;\n c.bloomStrength = 0.35;\n c.bloomRadius = 0.6;\n c.bloomThreshold = 0.55;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n Kaleidoscope: () => {\n const c = PRESETS[\"Wave 3\"]();\n const w = c.waves[0];\n w.paletteSource = \"kaleidoscope\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.12;\n c.grain = 0.5;\n return c;\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"presets.js","names":[],"sources":["../src/presets.ts"],"sourcesContent":["/**\n * Built-in presets: each a complete studio config (scene + one or more waves) in the wave model.\n * IP-clean — no copyrighted assets. The studio layers its own extra presets (and its historical\n * \"Stripe *\" display names) on top; see apps/studio/src/presets.ts.\n */\nimport { createDefaultConfig, makeStops, makeWaveSpread } from \"./config/model\";\nimport type {\n NoiseBand,\n ParticlesConfig,\n StudioConfig,\n WaveInteractionConfig,\n} from \"./config/model\";\n\nconst RAD = 180 / Math.PI;\n\n/**\n * Reusable particle STYLES applied to a wave's `particles` block. Surfaced in the studio's per-wave\n * Particles folder (the \"style\" picker) and used by the \"Particle Zoo\" showcase preset. Each is a dust\n * look independent of the wave it rides — clone before mutating (`structuredClone`).\n */\nexport const PARTICLE_PRESETS: Record<string, ParticlesConfig> = {\n Glitter: {\n count: 16000,\n size: 2.6,\n seed: 7,\n color: \"#ffdca8\",\n shape: \"glitter\",\n edgeBias: 1,\n drift: 300,\n bias: -0.6,\n twinkle: 0.8,\n },\n Embers: {\n count: 16000,\n size: 3.6,\n seed: 7,\n color: \"#ff8a2c\",\n color2: \"#ffe19a\",\n shape: \"soft\",\n edgeBias: 0.15,\n drift: 40,\n rise: 320,\n wander: 120,\n twinkle: 0.85,\n life: 6,\n },\n Snow: {\n count: 12000,\n size: 3,\n seed: 4,\n color: \"#eef4ff\",\n color2: \"#bcd0ee\",\n shape: \"soft\",\n edgeBias: 0.1,\n drift: 20,\n rise: -260,\n wander: 80,\n twinkle: 0.5,\n life: 8,\n },\n Sparks: {\n count: 9000,\n size: 5,\n seed: 9,\n color: \"#ffd27a\",\n color2: \"#fff4d0\",\n shape: \"streak\",\n edgeBias: 1,\n drift: 620,\n rise: 90,\n wander: 20,\n bias: -0.4,\n twinkle: 0.9,\n life: 2.5,\n },\n Fireflies: {\n count: 2200,\n size: 5,\n seed: 11,\n color: \"#dfff9a\",\n color2: \"#8fd94a\",\n shape: \"star\",\n edgeBias: 0.1,\n drift: 25,\n swirl: 0.3,\n wander: 170,\n twinkle: 1,\n life: 5,\n },\n Bubbles: {\n count: 3000,\n size: 7,\n sizeJitter: 0.9,\n seed: 6,\n color: \"#cdeefb\",\n color2: \"#8fd0e6\",\n shape: \"ring\",\n edgeBias: 0.2,\n drift: 30,\n rise: 240,\n wander: 60,\n twinkle: 0.3,\n life: 7,\n },\n};\n\n/** Degrees → radians (for the Particle Zoo's per-wave rotation, authored in degrees). */\nconst rad = (deg: number): number => (deg * Math.PI) / 180;\n\n/** Identity, but it pins a Zoo entry's inline interaction literal to {@link WaveInteractionConfig}:\n * without it the entries are merely INFERRED, so `source` / `target` widen to `string` (needing an\n * `as const` per field) and a misspelt knob would slip through unchecked. */\nconst interaction = (it: WaveInteractionConfig): WaveInteractionConfig => it;\n\n/** Clamp a signed magnitude to ±m (used to cap the Zoo's long-range particle motion terms). */\nconst capMag = (v: number | undefined, m: number): number | undefined =>\n v == null ? v : Math.sign(v) * Math.min(Math.abs(v), m);\n\n/** Build a preset from a set of wave parameters. rotation/hue are given in RADIANS and\n * converted to degrees. All presets are solid-theme, so they reuse the hero palette +\n * surfaceColor fibers (600/0.2) and sheen 0, like the hero. camTarget/zoom frame the\n * wave (we pan the look-at to centre each one). */\nfunction buildPreset(p: {\n speed: number;\n contrast: number;\n sat: number;\n hueRad: number;\n dispX: number;\n dispZ: number;\n dispAmt: number;\n pos: [number, number, number];\n rotRad: [number, number, number];\n scale: [number, number, number];\n twF: [number, number, number];\n twP: [number, number, number];\n glow: [number, number, number];\n grain: number;\n blur: number;\n zoom: number;\n camTarget: [number, number];\n noiseBands?: NoiseBand[];\n twistMotion?: boolean;\n}): StudioConfig {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.speed = p.speed;\n w.colorContrast = p.contrast;\n w.colorSaturation = p.sat;\n w.hueShift = p.hueRad * RAD;\n w.displaceFrequency = { x: p.dispX, y: p.dispZ };\n w.displaceAmount = p.dispAmt;\n w.position = { x: p.pos[0], y: p.pos[1], z: p.pos[2] };\n w.rotation = { x: p.rotRad[0] * RAD, y: p.rotRad[1] * RAD, z: p.rotRad[2] * RAD };\n w.scale = { x: p.scale[0], y: p.scale[1], z: p.scale[2] };\n w.twistFrequency = { x: p.twF[0], y: p.twF[1], z: p.twF[2] };\n w.twistPower = { x: p.twP[0], y: p.twP[1], z: p.twP[2] };\n w.creaseLight = p.glow[0];\n w.creaseSharpness = p.glow[1];\n w.creaseSoftness = p.glow[2];\n if (p.noiseBands) w.noiseBands = p.noiseBands;\n if (p.twistMotion) w.twistMotion = true;\n c.grain = p.grain;\n c.blur = p.blur;\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: p.camTarget[0], y: p.camTarget[1], z: 0 };\n c.cameraZoom = p.zoom;\n return c;\n}\n\n/** Presets: each a complete studio config (scene + one or more waves) in the wave model. */\nexport const PRESETS: Record<string, () => StudioConfig> = {\n // The app's default wave: a centred, full-frame ribbon (window-independent framing).\n // Shown first and named \"Hero\"; several presets below derive from it.\n Hero: () =>\n buildPreset({\n speed: 0.04,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [380, -301.7, -11.1],\n rotRad: [-0.44959, -0.11759, 1.874407],\n scale: [9, 8, 5],\n twF: [-0.65, 0.41, -0.58],\n twP: [3.63, 0.7, 3.95],\n glow: [1.98, 0.806, 0.834],\n grain: 1.1,\n blur: 0.02,\n zoom: 0.55,\n camTarget: [-420, -200], // user-tuned default framing\n }),\n // Stripe's real hero, recreated faithfully: an orthographic ×10 scene that overflows the\n // frame, so only the twisted crop shows. This is the model's plain default config.\n \"Wave 2\": () => createDefaultConfig(),\n // camTarget on the waves below is a first-pass centring; tune per-wave. NOTE: Wave 4 also\n // uses a variant vertex shader (animated twist-X wobble) we don't fully replicate — its\n // STATIC frame is close, the motion differs.\n \"Wave 3\": () =>\n buildPreset({\n speed: 0.08,\n contrast: 1,\n sat: 1,\n hueRad: -0.00159265,\n dispX: 0.005831,\n dispZ: 0.016001,\n dispAmt: -7.821,\n pos: [-200.7, -65.4, -11.1],\n rotRad: [-2.875593, 3.095927, -2.925927],\n scale: [3, 3, 3],\n twF: [0.059, 0.32, -0.397],\n twP: [3.63, 0.44, 5.99],\n glow: [3.86, 0.923, 1],\n grain: 1.2,\n blur: 0.02,\n zoom: 1.3,\n camTarget: [-104, 13], // centred; zoomed in (wide/flat wave)\n }),\n \"Wave 4\": () =>\n buildPreset({\n speed: 0.0525,\n contrast: 0.969,\n sat: 1.383,\n hueRad: 0.0376991,\n dispX: 0.005,\n dispZ: 0.0212,\n dispAmt: 6.68,\n pos: [206.1, -438, -11.1],\n rotRad: [-0.666018, -0.031416, 0.779115],\n scale: [6.0501, 8.3983, 6.9854],\n twF: [-0.424, 0.024, -1.312],\n twP: [1.81, 0.94, 4.76],\n glow: [1.55, 1.174, 0.972],\n grain: 0.576,\n blur: 0,\n zoom: 0.9316,\n camTarget: [194, -402], // centred on the wave\n twistMotion: true, // variant vertex shader — animated twist-X wobble\n noiseBands: [\n {\n startX: 0.856,\n endX: 1,\n startY: 0,\n endY: 0.913,\n feather: 0.5,\n strength: 0.346,\n frequency: 1018,\n colorAttenuation: 1,\n parabolaPower: 0,\n },\n {\n startX: 0.038,\n endX: 0.538,\n startY: 0.105,\n endY: 1,\n feather: 0.3315,\n strength: 1,\n frequency: 190,\n colorAttenuation: 0,\n parabolaPower: 2.11,\n },\n ],\n }),\n // The dark-background hero: identical geometry/camera to the default hero, but theme\n // \"wireframe\" → the line shader on a dark page background, with grain 1.2. Same palette.\n Wireframe: () => {\n const c = createDefaultConfig();\n c.waves[0].theme = \"wireframe\";\n c.grain = 1.2;\n c.background = \"#0a2540\"; // dark navy page background\n c.transparentBackground = false;\n return c;\n },\n \"Neon Dark Multistrand\": () => {\n const c = createDefaultConfig();\n const w = c.waves[0];\n w.theme = \"wireframe\"; // line shader on the near-black background — neon wireframe look\n w.blendMode = \"additive\";\n w.palette = makeStops([\"#00f5d4\", \"#00bbf9\", \"#9b5de5\", \"#f15bb5\", \"#fee440\"]);\n w.creaseLight = 1.0;\n c.background = \"#05060c\";\n c.transparentBackground = false; // fill the dark bg so the neon lines read on black (not the page)\n c.waves = makeWaveSpread(w, 3); // three overlapping neon waves\n c.waveCount = 3;\n return c;\n },\n \"Mesh Gradient\": () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n c.grain = 0.3;\n c.blur = 0.008;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n \"Solar Bloom\": () => {\n // Radial gradient: a warm core blooming out to a deep-indigo edge. usePaletteTexture off so\n // our own stops map along the radial gradCoord instead of sampling the baked hero LUT.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"radial\";\n w.gradientShift = 0.14;\n w.palette = [\n { color: \"#fff3c4\", pos: 0 }, // warm-white core\n { color: \"#ffd166\", pos: 0.22 }, // gold\n { color: \"#ff8c42\", pos: 0.42 }, // orange\n { color: \"#ff5d8f\", pos: 0.62 }, // coral-pink\n { color: \"#a64dff\", pos: 0.82 }, // violet\n { color: \"#241246\", pos: 1 }, // deep indigo edge\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.01;\n // Deep warm radial vignette behind the bloom.\n c.background = \"#0a0714\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"radial\";\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#2a1330\", \"#08040f\"]);\n c.transparentBackground = false;\n return c;\n },\n \"Latte Ring\": () => {\n // A warm cream-and-gold ring of combed silk curling around a dark void — the crema swirl on a\n // latte. The camera ORBITS a wide radial fan (arc 286°) so its combed length sweeps across the\n // frame as a flowing arc instead of a head-on fan; a mesh gradient warms it gold→cream with an\n // orange edge, and a noise band frays the fibers finer toward the tips. Exercises the radial wave\n // mode + the particle layer (ambient field + shed-from-edge dust into the void).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n // A wide radial fan (fans the ribbon's length); the orbiting camera below crops it to a ring.\n w.radialAmount = 0.72;\n w.radialArc = 286;\n w.radialCenter = -160;\n w.radialRadius = 300;\n w.radialSpread = 1.47;\n w.rotation = { x: 0, y: 0, z: 0 };\n w.position = { x: -280, y: -320, z: 0 };\n w.scale = { x: 1.12, y: 1.12, z: 1.12 };\n w.opacity = 0.5;\n // A big broad swell (amount 101.73) with a fine second octave riding on it, so the ring's silk\n // ripples and folds as it turns; `speed` sets the swell's drift rate.\n w.displaceFrequency = { x: 0.0026, y: 0.0048 };\n w.displaceAmount = 101.73;\n w.detailAmount = 6;\n w.detailFrequency = 0.1;\n w.speed = 0.21;\n // Fine combed fibers; the noise band overrides them finer + wispier over the OUTER half (uv.y>0.5)\n // for organic variation instead of a uniform comb. (Bands override fiber params per uv region.)\n w.fiberCount = 110;\n w.fiberStrength = 0.95;\n w.creaseLight = 0.55;\n w.edgeFeather = 0.34;\n w.noiseBands = [\n {\n startX: 0,\n endX: 1,\n startY: 0.5,\n endY: 1,\n feather: 0.4,\n strength: 1,\n frequency: 300,\n colorAttenuation: 0.85,\n parabolaPower: 2.5,\n },\n ];\n // Mesh gradient: gold + cream dominant with an orange accent — the latte's crema tones. A 2D\n // colour field (not a length-wise ramp) so the warmth pools within the fibers.\n w.usePaletteTexture = false;\n w.gradientType = \"mesh\";\n w.meshGradientSoftness = 0.7;\n w.meshGradientPoints = [\n { color: \"#f3c06a\", x: 0.5, y: 0.1, influence: 0.7 }, // warm gold\n { color: \"#f9edd4\", x: 0.34, y: 0.36, influence: 0.62 }, // hot cream\n { color: \"#e6923a\", x: 0.15, y: 0.55, influence: 0.5 }, // orange accent\n { color: \"#f1d49a\", x: 0.82, y: 0.42, influence: 0.55 }, // gold\n { color: \"#ece7d8\", x: 0.58, y: 0.88, influence: 0.72 }, // cool cream\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.15;\n // Orbit the camera around the fan so its length reads as a sweeping arc wrapping the void.\n c.cameraDistance = 600;\n c.cameraPosition = { x: -854.327, y: 135.331, z: 478.048 };\n c.cameraTarget = { x: -720.043, y: 10.575, z: -93.27 };\n c.cameraZoom = 1.222;\n // Scene: a near-black void with gentle bloom on the silk + dust.\n c.background = \"#050404\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.25;\n c.blur = 0;\n c.bloomStrength = 0.18;\n c.bloomRadius = 0.55;\n c.bloomThreshold = 0.72;\n // Golden dust shed off the ring's own edge into the void (biased to one flank). Per-wave: it rides\n // this wave's deform and drifts outward from it. edgeBias 1 = spawn on the rim (the shed look).\n w.particles = {\n count: 20000,\n size: 2.9,\n color: \"#ffdca8\",\n seed: 7,\n twinkle: 0.8,\n edgeBias: 1,\n drift: 490,\n bias: -0.6,\n };\n return c;\n },\n \"Particle Zoo\": () => {\n // One scene demoing every particle STYLE — each wave sheds a different kind of dust off its own\n // surface (embers / snow / sparks / fireflies / bubbles). Every wave is a DIFFERENT shape family\n // too (dome / flat sheet / radial fan / helix coil / twist curl), spread apart so none overlap and\n // the frame stays full. The showcase for the per-wave particle system; styles come from\n // PARTICLE_PRESETS (the studio \"preset style\" picker).\n //\n // Each specimen also answers the cursor a DIFFERENT way — agitate / push / click-ripple /\n // param-binding / drag-wake — so the scene doubles as the interactivity showcase, and each one\n // shows its dust reacting alongside its ribbon.\n const c = createDefaultConfig();\n c.background = \"#05070d\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n c.grain = 0.26;\n c.blur = 0;\n c.bloomStrength = 0.3;\n c.bloomThreshold = 0.66; // high, so the central sparks fan glows without blowing the middle out\n c.cameraPosition = { x: 100, y: 0, z: 5000 };\n c.cameraTarget = { x: 0, y: 0, z: 0 };\n c.cameraZoom = 0.6;\n // Shared cursor. A TIGHT radius so hovering one specimen doesn't stir its neighbours: the waves\n // sit ~700-1400 world units apart in a 1250-tall frame, and radius is a fraction of viewport\n // height doubled into NDC-y, so 0.22 → 0.44 NDC-y ≈ one specimen. Touch is opt-in and this is a\n // showcase, so turn it on (listeners are passive — it does not block page scrolling).\n c.interaction = { enabled: true, radius: 0.22, ribbonFlow: 0.5, touch: true };\n // Five well-separated waves, each a DIFFERENT shape family so the silhouettes read as distinct,\n // not five copies of one ribbon. Frame at cameraZoom 0.6 spans ~2222×1250 world units centred on\n // the origin (x ∈ [-1111, 1111], y ∈ [-625, 625], orthographic → screen-xy = world-xy), so the\n // positions/scales below keep the meshes from overlapping. `shape` carries the per-family knobs\n // (displace / twist / helix / radial); `rot` is in degrees. style, palette, opacity as before.\n const zoo = [\n {\n // Embers rising off a broad DOME — low-frequency, high-amplitude displacement mound.\n style: \"Embers\",\n // STIR THE FIRE — cursor agitation. The churn octave stirs the mound and the embers riding\n // it scatter; shove high so the ones already airborne get swept along too.\n interact: interaction({ hover: { agitate: 34, lighten: 0.25 } }),\n shove: 2.5,\n palette: [\"#ff7a1e\", \"#ffd27a\", \"#c23a12\"],\n pos: [-720, 285],\n scale: [1.0, 1.0, 1.0],\n rot: [-20, 0, -14],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.0024, y: 0.0065 },\n displaceAmount: 118,\n twistPower: { x: 2.4, y: 2.4, z: 3.2 },\n },\n },\n {\n // Snow settling on a near-flat drifting SHEET — tilted toward the camera, barely rippled.\n style: \"Snow\",\n // PRESS INTO THE DRIFT — a NEGATIVE push, so the cursor dents the sheet away from you like a\n // palm in deep snow, and the settled flakes sink into the hollow with it. Slow smoothing so\n // the hollow follows heavily; low shove because falling snow shouldn't whip around.\n interact: interaction({ hover: { push: -22, smoothing: 0.45 } }),\n shove: 0.6,\n palette: [\"#8fb8e8\", \"#e8f1fb\", \"#aeb9d6\"],\n pos: [700, 320],\n scale: [1.1, 1.0, 0.9],\n rot: [-62, 0, 12],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.006, y: 0.013 },\n displaceAmount: 22,\n twistPower: { x: 0.8, y: 0.8, z: 1.0 },\n },\n },\n {\n // Sparks bursting off a RADIAL FAN — the one spiky, fanned silhouette; dimmed so it glows\n // without blowing the centre out.\n style: \"Sparks\",\n // STRIKE IT — click/tap only, no hover field. A ring radiates from the hit and, at full\n // shove, visibly blows OUT through the spark burst instead of stopping at the ribbon: the\n // clearest demo of dust reacting to the pointer.\n interact: interaction({ press: { ripple: 46 } }),\n shove: 4,\n palette: [\"#ffd27a\", \"#fff4d0\", \"#c8853a\"],\n pos: [0, -30],\n scale: [0.85, 0.85, 0.85],\n rot: [0, 0, 0],\n opacity: 0.32,\n shape: {\n radialAmount: 0.82,\n radialArc: 118,\n radialCenter: 90, // fan points up\n radialRadius: 40,\n radialSpread: 1.0,\n displaceAmount: 30,\n },\n },\n {\n // Fireflies wandering around a HELIX coil — a screw-thread column of light.\n style: \"Fireflies\",\n // WIND THE COIL — a BINDING, not a pointer field. Hovering turns the helix a full rotation\n // and the fireflies ride round with it, because each field mirrors its wave's live shape\n // uniforms every frame. Deliberately the one specimen with no `hover` block: its particle\n // program never compiles the pointer path, yet its dust still follows the wave.\n interact: interaction({\n bindings: [{ source: \"hover\", target: \"helixPhase\", to: 360, smoothing: 0.5 }],\n }),\n palette: [\"#3d5a24\", \"#7bd23a\", \"#40521f\"],\n pos: [-560, -300],\n scale: [1.25, 1.25, 1.25],\n rot: [0, 0, 8],\n opacity: 0.72,\n shape: {\n helixTurns: 3,\n helixRadius: 44,\n helixRoll: 1,\n displaceAmount: 8,\n twistPower: { x: 1.0, y: 1.0, z: 1.2 },\n },\n },\n {\n // Bubbles rising off a strongly TWISTED curl — an S-ribbon coiling on its length.\n style: \"Bubbles\",\n // DRAG THROUGH THE WATER — the drag-wake. Sweep the cursor and the ribbon behind it is\n // pulled into a trailing trough that heals when you stop, with the bubbles streaming into\n // the wake; `thin` adds a watery translucency right under the cursor.\n interact: interaction({ hover: { wake: 30, thin: 0.45 } }),\n shove: 1.6,\n palette: [\"#2e8fb0\", \"#7fd4e8\", \"#1a5a6e\"],\n pos: [660, -270],\n scale: [1.0, 1.15, 1.0],\n rot: [-16, 0, -20],\n opacity: 0.72,\n shape: {\n displaceFrequency: { x: 0.005, y: 0.011 },\n displaceAmount: 46,\n twistFrequency: { x: -0.06, y: 0.09, z: -0.5 },\n twistPower: { x: 4.0, y: 6.0, z: 9.0 },\n },\n },\n ];\n const base = c.waves[0];\n c.waves = zoo.map((z, i) => {\n const w = structuredClone(base);\n w.usePaletteTexture = false;\n w.gradientType = \"linear\";\n w.gradientAngle = 0;\n w.palette = makeStops(z.palette);\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1.1;\n w.opacity = z.opacity;\n w.scale = { x: z.scale[0], y: z.scale[1], z: z.scale[2] };\n w.rotation = { x: rad(z.rot[0]), y: rad(z.rot[1]), z: rad(z.rot[2]) };\n w.position = { x: z.pos[0], y: z.pos[1], z: 0 };\n w.seed = i * 7;\n w.speed = 0.05;\n Object.assign(w, z.shape); // the wave's distinct shape family (displace / twist / helix / radial)\n // Each PARTICLE_PRESET is tuned as a standalone hero cloud whose dust flings far; here every\n // specimen should sit ON the wave that emits it, reading as dust clinging to the surface rather\n // than a cloud floating nearby. Pull the motion terms right down (a small fraction of the hero\n // reach + tight clamps) so the dust overlaps its own wave; the styles still read apart by sprite\n // shape / colour / wave shape.\n const p = structuredClone(PARTICLE_PRESETS[z.style]);\n const PULL = 0.13; // fraction of each preset's hero-scale reach kept here\n if (p.drift != null) p.drift = capMag(p.drift * PULL, 24);\n if (p.rise != null) p.rise = capMag(p.rise * PULL, 28);\n if (p.wander != null) p.wander = capMag(p.wander * PULL, 15);\n if (p.swirl != null) p.swirl = capMag(p.swirl * PULL, 0.05);\n if (z.shove != null) p.pointerShove = z.shove;\n w.particles = p;\n // One distinct interaction per specimen (agitate / push / ripple / binding / wake) — see each\n // entry above. Absent would mean inert, so only assign what the entry authored.\n if (z.interact) w.interaction = structuredClone(z.interact);\n return w;\n });\n c.waveCount = c.waves.length;\n return c;\n },\n Holographic: () => {\n // Conic gradient: an iridescent oil-slick sweep. The palette wraps (first ≈ last stop) so\n // the conic seam is invisible.\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.usePaletteTexture = false;\n w.gradientType = \"conic\";\n w.gradientShift = 0.08;\n w.palette = [\n { color: \"#8ef6e4\", pos: 0 }, // mint (seam)\n { color: \"#6ec3ff\", pos: 0.18 }, // sky\n { color: \"#9b8cff\", pos: 0.36 }, // periwinkle\n { color: \"#ff8ad8\", pos: 0.54 }, // pink\n { color: \"#ffd98e\", pos: 0.72 }, // peach\n { color: \"#a0f0c8\", pos: 0.88 }, // seafoam\n { color: \"#8ef6e4\", pos: 1 }, // mint again (seamless wrap)\n ];\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.04;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.12;\n c.grain = 0.28;\n c.blur = 0.01;\n // Subtle deep teal → violet wash behind the iridescence.\n c.background = \"#05060c\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"linear\";\n c.backgroundGradientAngle = 135;\n c.backgroundGradientSource = \"stops\";\n c.backgroundPalette = makeStops([\"#04121a\", \"#0a0518\"]);\n c.transparentBackground = false;\n return c;\n },\n Aurora: () => {\n // Mesh gradient: a moody aurora — teals/greens drifting into violet over a night-sky base\n // (distinct from the brighter iOS-style \"Mesh Gradient\").\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a1f3c\", x: 0.08, y: 0.12, influence: 0.62 },\n { color: \"#1fddb0\", x: 0.3, y: 0.7, influence: 0.78 },\n { color: \"#57f5a3\", x: 0.58, y: 0.86, influence: 0.7 },\n { color: \"#3a86ff\", x: 0.82, y: 0.55, influence: 0.62 },\n { color: \"#a15cff\", x: 0.5, y: 0.32, influence: 0.7 },\n { color: \"#071433\", x: 0.92, y: 0.08, influence: 0.6 },\n ];\n w.meshGradientSoftness = 0.72;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.18;\n w.fiberStrength = 0.12;\n c.grain = 0.3;\n c.blur = 0.008;\n // Dark night-sky MESH backdrop (also shows off the mesh background type).\n c.background = \"#03060f\";\n c.backgroundMode = \"gradient\";\n c.backgroundGradientType = \"mesh\";\n c.backgroundMeshPoints = [\n { color: \"#02040c\", x: 0.15, y: 0.85, influence: 0.7 },\n { color: \"#08243a\", x: 0.5, y: 0.5, influence: 0.75 },\n { color: \"#0a0f2e\", x: 0.85, y: 0.7, influence: 0.7 },\n { color: \"#04121a\", x: 0.7, y: 0.2, influence: 0.6 },\n { color: \"#000208\", x: 0.12, y: 0.12, influence: 0.6 },\n ];\n c.backgroundMeshSoftness = 0.75;\n c.transparentBackground = false;\n return c;\n },\n Palestine: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"palestine\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1;\n w.colorSaturation = 1;\n c.grain = 0.35;\n c.background = \"#f2efe8\";\n c.transparentBackground = true;\n return c;\n },\n Spain: () => {\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.paletteSource = \"spain\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.18;\n w.colorSaturation = 1.25;\n w.creaseLight = 1.6; // moderate crease-light: rich crimson without washing to salmon (Hero's is 1.98)\n c.grain = 0.3;\n c.background = \"#1a0608\"; // deep oxblood stage\n c.backgroundMode = \"color\";\n c.transparentBackground = false; // opaque, so the dark stage makes the flag pop\n return c;\n },\n \"Vaporwave Sunset\": () => {\n // The Hero wave re-posed/re-framed, plus the vaporwave palette.\n const c = PRESETS[\"Hero\"](); // the centred default \"Hero\" wave\n const w = c.waves[0];\n w.position.x = 525;\n w.rotation.x = -0.64 * RAD;\n w.rotation.z = 1.68 * RAD;\n w.paletteSource = \"vaporwave\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.08;\n w.colorSaturation = 1.15;\n w.creaseLight = 1.25;\n c.cameraZoom = 1.1;\n c.cameraTarget = { x: 150, y: 360, z: 0 };\n c.background = \"#09051f\";\n c.transparentBackground = false;\n return c;\n },\n Corkscrew: () => {\n // The helix mode, shown off on its own: `helixRoll` at 1 rolls the ribbon's cross-section in\n // step with the sweep, so the flat strip becomes an auger blade winding around its own length\n // axis, and `helixRadius` lifts that blade off the axis so the turns read as a screw thread\n // rather than a flat twist. No twist at all — this shape is unreachable with twistFrequency,\n // whose expStep angle is monotone and can only ramp once (see the helix docs in config/model).\n const c = PRESETS[\"Hero\"]();\n const w = c.waves[0];\n w.helixTurns = 5;\n w.helixRadius = 45;\n w.helixRoll = 1;\n w.helixPhase = 0;\n w.twistFrequency = { x: 0, y: 0, z: 0 };\n w.twistPower = { x: 4, y: 4, z: 2 };\n // A slow swell along the blade so it breathes; the corkscrew itself is static geometry.\n w.displaceAmount = 16;\n w.displaceFrequency = { x: 0.006, y: 0.0008 };\n w.speed = 0.1;\n w.position = { x: 0, y: 0, z: 0 };\n w.rotation = { x: 0, y: 0, z: 12 }; // tilt so it climbs across the frame\n w.scale = { x: 3, y: 3, z: 1.5 };\n // Mesh gradient: the colour field runs along the blade, so each turn picks up a different part\n // of the spectrum instead of the one hue a linear stop ramp would give.\n w.gradientType = \"mesh\";\n w.meshGradientPoints = [\n { color: \"#0a84ff\", x: 0.06, y: 0.9, influence: 0.68 },\n { color: \"#64d2ff\", x: 0.88, y: 0.92, influence: 0.72 },\n { color: \"#bf5af2\", x: 0.5, y: 0.64, influence: 0.58 },\n { color: \"#ff375f\", x: 0.1, y: 0.14, influence: 0.7 },\n { color: \"#ff9f0a\", x: 0.84, y: 0.12, influence: 0.74 },\n { color: \"#30d158\", x: 0.94, y: 0.5, influence: 0.54 },\n ];\n w.meshGradientSoftness = 0.68;\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.06;\n w.colorSaturation = 1.12;\n w.fiberStrength = 0.14;\n // Framed down the axis rather than side-on: the coil reads as a screw receding into the frame,\n // and each turn shows its blade face instead of an edge. cameraDistance is the orbit radius\n // (= |position − target|); the camera is orthographic, so it's the rig's dolly, not the scale —\n // cameraZoom sets that.\n c.cameraPosition = { x: -526.009, y: -285.284, z: -425.489 };\n c.cameraTarget = { x: -95.046, y: -17.053, z: -105.608 };\n c.cameraDistance = 600;\n c.cameraZoom = 1.176;\n c.grain = 0.3;\n c.blur = 0.008;\n c.bloomStrength = 0.35;\n c.bloomRadius = 0.6;\n c.bloomThreshold = 0.55;\n c.background = \"#070914\";\n c.backgroundMode = \"color\";\n c.transparentBackground = false;\n return c;\n },\n Kaleidoscope: () => {\n const c = PRESETS[\"Wave 3\"]();\n const w = c.waves[0];\n w.paletteSource = \"kaleidoscope\";\n w.blendMode = \"normal\";\n w.hueShift = 0;\n w.colorContrast = 1.05;\n w.colorSaturation = 1.12;\n c.grain = 0.5;\n return c;\n },\n};\n"],"mappings":"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"}
@@ -1,8 +1,11 @@
1
- import { CameraFit, StudioConfig, WaveConfig } from "../config/model.js";
1
+ import { BlendMode, CameraFit, StudioConfig, WaveConfig } from "../config/model.js";
2
2
  import { WaveGeometry } from "./WaveGeometry.js";
3
- import { ParticleField } from "./particleField.js";
3
+ import { ParticleFrame } from "./particleField.js";
4
+ import { TiltStatus } from "./tilt.js";
4
5
  import { InteractionController } from "./interaction.js";
5
6
  import * as THREE from "three";
7
+ import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
8
+ import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
6
9
  //#region src/renderer/WaveRenderer.d.ts
7
10
  /** Reference frame (world units) the orthographic camera fills at cameraZoom 1. The wave is
8
11
  * framed by mapping this FRAME_W × FRAME_H rectangle (centred on cameraTarget) onto the canvas,
@@ -61,15 +64,50 @@ declare class WavePalette {
61
64
  private clearVideo;
62
65
  dispose(): void;
63
66
  }
67
+ /**
68
+ * The uniform surface both backends expose. The GLSL path holds `THREE.IUniform`s; the TSL path
69
+ * holds TSL uniform nodes and array wrappers. Both are written the same way — `u.uFoo.value = x`,
70
+ * `u.uColors.value[i].set(...)` — which is what lets the ~116 config-sync writes in `refresh()`
71
+ * stay backend-agnostic.
72
+ */
73
+ type WaveUniforms = Record<string, {
74
+ value: unknown;
75
+ }>;
76
+ /** A wave material, whichever backend built it. */
77
+ type WaveMaterial = THREE.Material & {
78
+ uniforms: WaveUniforms;
79
+ };
80
+ /**
81
+ * A wave's particle field, whichever backend built it.
82
+ *
83
+ * The TSL field is a `THREE.Sprite` where the GLSL one is a `THREE.Points` (WebGPU has no point
84
+ * size), so the scene node is reached through `object` rather than either concrete type.
85
+ */
86
+ interface WaveParticleField {
87
+ readonly object: THREE.Object3D;
88
+ sync(cfg: NonNullable<WaveConfig["particles"]>, loopSeconds: number): void;
89
+ frame(f: ParticleFrame, pixelRatio: number): void;
90
+ configure(shape: {
91
+ defines?: Record<string, string>;
92
+ uniforms?: Record<string, {
93
+ value: unknown;
94
+ }>;
95
+ matrixWorld: THREE.Matrix4;
96
+ speed: number;
97
+ seed: number;
98
+ }): void;
99
+ setTime(t: number): void;
100
+ dispose(): void;
101
+ }
64
102
  type Wave = {
65
103
  mesh: THREE.Mesh;
66
- material: THREE.ShaderMaterial;
104
+ material: WaveMaterial;
67
105
  geometry: WaveGeometry;
68
106
  /** This wave's own 2D palette texture + optional video. */
69
107
  palette: WavePalette;
70
108
  /** This wave's own particle / dust field — created when its `particles.count` first goes >0,
71
109
  * disposed at 0 / absent (the WavePalette lifecycle pattern). Undefined = no dust for this wave. */
72
- particleField?: ParticleField;
110
+ particleField?: WaveParticleField;
73
111
  };
74
112
  /** Convert an sRGB hex string to a linear-space RGB vector (three's ColorManagement does the
75
113
  * sRGB→linear conversion on parse). Exported for the studio subclass's live light-uniform push. */
@@ -81,11 +119,17 @@ declare function hexToLinearVec3(hex: string, target: THREE.Vector3): THREE.Vect
81
119
  */
82
120
  declare class WaveRenderer {
83
121
  readonly renderer: THREE.WebGLRenderer;
122
+ /**
123
+ * Whether this renderer can draw yet. The WebGL backend is ready the moment it is constructed;
124
+ * the WebGPU subclass has to `await renderer.init()` first, and `renderOnce()` throws before that.
125
+ * The base class is always ready, so the WebGL path is unaffected.
126
+ */
127
+ protected ready: boolean;
84
128
  protected readonly scene: THREE.Scene<THREE.Object3DEventMap>;
85
129
  protected readonly camera: THREE.OrthographicCamera;
86
130
  protected readonly group: THREE.Group<THREE.Object3DEventMap>;
87
- private readonly composer;
88
- private readonly postPass;
131
+ protected composer: EffectComposer;
132
+ protected postPass: ShaderPass;
89
133
  /** Optional bloom pass — created lazily when bloomStrength first goes >0, removed at 0. */
90
134
  private bloomPass?;
91
135
  private ditherPass?;
@@ -118,8 +162,17 @@ declare class WaveRenderer {
118
162
  private readonly clipSphere;
119
163
  private readonly clipTmpA;
120
164
  private readonly clipTmpB;
121
- /** Created by syncInteraction() when interaction turns on, disposed when it turns off. */
165
+ /** Created by loadInteraction() once the runtime chunk lands, disposed when interaction turns off.
166
+ * Stays undefined for the frames between the config turning it on and the chunk arriving. */
122
167
  protected interaction?: InteractionController;
168
+ /** The chunk itself, kept for the applier tables. Set and cleared with `interaction`. */
169
+ private interactionModule?;
170
+ /** A fetch already in flight, so a per-frame refresh can't start a second one. */
171
+ private interactionLoading;
172
+ /** setInteractionInput() calls made while the chunk was in flight, replayed once it lands. */
173
+ private stagedInputs?;
174
+ /** Set by dispose(), so an in-flight chunk can't attach listeners to a dead renderer. */
175
+ private disposed;
123
176
  /** Extra ortho-zoom MULTIPLIER from a cameraZoom binding (1 = none); applied in applyZoom().
124
177
  * Protected so the studio's writeCameraToConfig() can divide it back out (keep it out of config). */
125
178
  protected interactionZoom: number;
@@ -150,6 +203,17 @@ declare class WaveRenderer {
150
203
  * this unset and continue to resize responsively with their container and device DPR. */
151
204
  private outputSize?;
152
205
  constructor(container: HTMLElement, config: StudioConfig, options?: WaveRendererOptions);
206
+ /**
207
+ * Build the backing three renderer. Overridden by the WebGPU subclass; called from the
208
+ * constructor, so it must not depend on subclass FIELDS (prototype methods are available during
209
+ * `super()`, field initialisers are not).
210
+ */
211
+ protected createRenderer(): THREE.WebGLRenderer;
212
+ /**
213
+ * Await any asynchronous backend startup. A no-op on WebGL; the WebGPU subclass overrides it to
214
+ * `await renderer.init()` and then draw the first frame. Safe to call more than once.
215
+ */
216
+ init(): Promise<void>;
153
217
  private get segments();
154
218
  private makeUniforms;
155
219
  /** Vertex-shader #defines for a wave: TWIST_MOTION (per-wave animated twist wobble) and
@@ -157,6 +221,19 @@ declare class WaveRenderer {
157
221
  * object compiles the default (linear-time) program. */
158
222
  private waveDefines;
159
223
  private addWave;
224
+ /**
225
+ * Build the material for one wave. The GLSL backend compiles a ShaderMaterial with `#define`
226
+ * variants; the WebGPU subclass builds a TSL node graph instead. Both expose the same `uniforms`
227
+ * surface, so everything downstream in `refresh()` is shared.
228
+ */
229
+ protected createWaveMaterial(sc: WaveConfig | undefined): WaveMaterial;
230
+ /**
231
+ * Re-select this wave's shader variant when its config changes shape (theme, twist motion, helix,
232
+ * …). Returns true if the material needs recompiling. The GLSL backend swaps `defines` and the
233
+ * fragment source; the WebGPU subclass rebuilds the node graph, since a TSL variant is a
234
+ * different graph rather than a different define set.
235
+ */
236
+ protected applyWaveVariant(wave: Wave, sc: WaveConfig): boolean;
160
237
  /**
161
238
  * Apply config.blendMode to a material. "squared" (the default) is the hero blend:
162
239
  * CustomBlending with AddEquation, src = SrcColorFactor, dst = ZeroFactor, so the
@@ -166,7 +243,7 @@ declare class WaveRenderer {
166
243
  * fragment shaders premultiply their output when Three injects PREMULTIPLIED_ALPHA.
167
244
  * Returns true if material state changed (caller flags needsUpdate).
168
245
  */
169
- private applyBlendMode;
246
+ protected applyBlendMode(material: WaveMaterial, mode: BlendMode): boolean;
170
247
  private disposeWaves;
171
248
  /**
172
249
  * Reconcile the wave pool to `waveCount` WITHOUT tearing everything down:
@@ -185,6 +262,22 @@ declare class WaveRenderer {
185
262
  /** Rebuild geometry + waves (call when waveCount or quality changes). */
186
263
  rebuild(): void;
187
264
  private applyBackground;
265
+ /**
266
+ * Build a wave's particle field. The GLSL backend returns a `THREE.Points` field; the TSL backend
267
+ * returns an instanced-sprite one wired to this wave's own uniform registry.
268
+ */
269
+ protected createParticleField(_wave: Wave, _sc: WaveConfig, onReady: () => void): WaveParticleField;
270
+ /**
271
+ * Largest texture edge the backend will accept, used to cap the background canvas. WebGL reports
272
+ * it on `capabilities`; WebGPU has no such object, so the subclass reads the device limit.
273
+ */
274
+ protected maxTextureSize(): number;
275
+ /**
276
+ * Called whenever the scene background changes. A no-op on WebGL, where `EffectComposer`'s
277
+ * RenderPass reads the scene afresh each frame; the WebGPU subclass rebuilds its post chain,
278
+ * because a `pass()` node captures the background state at the point its render context is built.
279
+ */
280
+ protected onBackgroundChanged(): void;
188
281
  private applyColorBackground;
189
282
  private applyGradientBackground;
190
283
  /** Image mode: a live video, a user-loaded image, or a built-in map. `matte` shows while an
@@ -265,6 +358,12 @@ declare class WaveRenderer {
265
358
  /** Advance the per-frame time uniforms (geometry itself is static). Time model:
266
359
  * time = elapsed·introTimeRamp + timeOffset — the ramp eases the animation in on load. */
267
360
  private updateTime;
361
+ /** Draw the composed frame. WebGL runs the EffectComposer; WebGPU runs a node post chain. */
362
+ protected renderComposed(): void;
363
+ /** Resize the post chain's render targets. */
364
+ protected resizePost(w: number, h: number, dpr: number): void;
365
+ /** Release the post chain's GPU resources. */
366
+ protected disposePost(): void;
268
367
  /** Render exactly one frame at the current time. */
269
368
  renderOnce(): void;
270
369
  /** Insert / sync / remove EACH wave's particle field — mirrors applyBloom's lazy lifecycle, per wave.
@@ -277,12 +376,28 @@ declare class WaveRenderer {
277
376
  * uniforms + matrixWorld) so the dust rides the SAME deform as the ribbon. Runs after refresh()'s
278
377
  * per-wave loop so the wave transforms/uniforms are current. No allocation; a no-op when off. */
279
378
  private updateSceneFx;
280
- /** The shape-affecting subset of waveDefines() (what the shared waveShape reads) used to compile
281
- * each wave's particle shader to match its own deform. */
379
+ /** The subset of waveDefines() the particle program shares: the shape gates (what waveShape reads,
380
+ * so the dust matches its wave's deform) plus the pointer gates (what pointerField reads, so the
381
+ * dust reacts to the same cursor). Every one of these is derived from CONFIG alone — live input
382
+ * must never reach here, or each frame would flip the define set and recompile the point program. */
282
383
  private shapeDefines;
283
384
  /** Create/dispose the interaction controller as config toggles interaction on/off. Called from
284
385
  * refresh(); the compiled define set (POINTER_FX etc.) is handled separately by waveDefines(). */
285
386
  private syncInteraction;
387
+ /**
388
+ * Fetch the interactivity runtime and attach it. Deliberately a DYNAMIC import: the controller,
389
+ * its listeners, the applier tables and the tilt sensor are ~3.8 KB gzipped that a scene with no
390
+ * `interaction` block never runs, and a static import would put all of it in every bundle. The
391
+ * cost is that interaction goes live a chunk-fetch after the first frame instead of on it —
392
+ * invisible in practice, since there is nothing to react to until a reader moves.
393
+ *
394
+ * A failed fetch is not fatal: the wave renders exactly as an inert one does, which is the same
395
+ * thing that happens on a config with no interaction at all.
396
+ */
397
+ private loadInteraction;
398
+ /** Hook for subclasses that hold state the controller must be told about once it exists (the
399
+ * studio's scroll preview). No-op in the base renderer. */
400
+ protected onInteractionReady(): void;
286
401
  /** Per-frame interaction write: dynamic pointer-field uniforms + bindings. No-op without a
287
402
  * controller. While capturing it writes the REST state instead (pointer field zeroed, every bound
288
403
  * param at its authored base) — merely skipping the write would freeze whatever live hover/scroll
@@ -302,8 +417,27 @@ declare class WaveRenderer {
302
417
  /** Evaluate bindings via the applier tables: value = mix(from ?? base, to, smoothedSource). Scene
303
418
  * bindings drive scene params; each wave's bindings drive that wave's uniforms. */
304
419
  private applyBindings;
305
- /** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off. */
420
+ /** Feed a `custom:<name>` interaction input (developer API). No-op when interaction is off.
421
+ * Values fed before the interaction chunk lands are STAGED (last one per name wins) and replayed
422
+ * when it does — otherwise a one-shot input sent right after `onReady` would vanish into the
423
+ * fetch window. */
306
424
  setInteractionInput(name: string, value: number): void;
425
+ /**
426
+ * Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal
427
+ * permission dialog — so this belongs to a page where tilt is the point, not to a decorative
428
+ * background, which should simply go without tilt there. Nothing calls this for you.
429
+ *
430
+ * CALL IT FROM A USER GESTURE: iOS 13+ only grants the sensor from inside a tap handler, and
431
+ * awaiting anything before it (a fetch, a timeout) spends the gesture. Resolves true once
432
+ * readings can flow — false when
433
+ * the platform has no sensor, the scene declares no `interaction.tilt`, or the reader refused.
434
+ * Where no permission is required the sensor is already live and this resolves true.
435
+ */
436
+ enableTilt(): Promise<boolean>;
437
+ /** Where the tilt sensor stands. `"prompt"` is exactly when a tap-to-enable affordance helps. */
438
+ tiltStatus(): TiltStatus;
439
+ /** Take the next orientation reading as the neutral pose — for when the reader has changed grip. */
440
+ recenterTilt(): void;
307
441
  /** Re-evaluate play/pause after `config.paused` changes. */
308
442
  refreshPlayback(): void;
309
443
  /** Jump the camera to the config's authored framing (cameraPosition / cameraTarget /
@@ -355,5 +489,5 @@ declare class WaveRenderer {
355
489
  dispose(): void;
356
490
  }
357
491
  //#endregion
358
- export { FRAME_H, FRAME_W, WaveRenderer, WaveRendererOptions, frameZoom, hexToLinearVec3 };
492
+ export { FRAME_H, FRAME_W, WaveMaterial, WaveParticleField, WaveRenderer, WaveRendererOptions, WaveUniforms, frameZoom, hexToLinearVec3 };
359
493
  //# sourceMappingURL=WaveRenderer.d.ts.map