@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.
Files changed (53) hide show
  1. package/dist/config/model.d.ts +209 -6
  2. package/dist/config/model.js +85 -3
  3. package/dist/config/model.js.map +1 -1
  4. package/dist/index.d.ts +3 -2
  5. package/dist/index.js +3 -2
  6. package/dist/presets.js +295 -0
  7. package/dist/presets.js.map +1 -1
  8. package/dist/renderer/WaveGeometry.js +21 -0
  9. package/dist/renderer/WaveGeometry.js.map +1 -1
  10. package/dist/renderer/WaveRenderer.d.ts +62 -0
  11. package/dist/renderer/WaveRenderer.js +349 -10
  12. package/dist/renderer/WaveRenderer.js.map +1 -1
  13. package/dist/renderer/WaveRendererGPU.js +32 -2
  14. package/dist/renderer/WaveRendererGPU.js.map +1 -1
  15. package/dist/renderer/interaction.js +12 -0
  16. package/dist/renderer/interaction.js.map +1 -1
  17. package/dist/renderer/particleField.js +26 -2
  18. package/dist/renderer/particleField.js.map +1 -1
  19. package/dist/renderer/particleFieldGPU.js +6 -0
  20. package/dist/renderer/particleFieldGPU.js.map +1 -1
  21. package/dist/renderer/shaders.js +748 -13
  22. package/dist/renderer/shaders.js.map +1 -1
  23. package/dist/renderer/tsl/dissolve.js +56 -0
  24. package/dist/renderer/tsl/dissolve.js.map +1 -0
  25. package/dist/renderer/tsl/particleMaterial.js +46 -8
  26. package/dist/renderer/tsl/particleMaterial.js.map +1 -1
  27. package/dist/renderer/tsl/uniforms.js +40 -1
  28. package/dist/renderer/tsl/uniforms.js.map +1 -1
  29. package/dist/renderer/tsl/waveMaterial.js +275 -26
  30. package/dist/renderer/tsl/waveMaterial.js.map +1 -1
  31. package/dist/renderer/tsl/waveShape.js +31 -10
  32. package/dist/renderer/tsl/waveShape.js.map +1 -1
  33. package/dist/renderer/wavePath.js +189 -0
  34. package/dist/renderer/wavePath.js.map +1 -0
  35. package/dist/shell/createWave.d.ts +23 -3
  36. package/dist/shell/createWave.js +5 -4
  37. package/dist/shell/createWave.js.map +1 -1
  38. package/dist/shell/probe.d.ts +28 -0
  39. package/dist/shell/probe.js +58 -11
  40. package/dist/shell/probe.js.map +1 -1
  41. package/dist/standalone/wave3d.standalone.js +3401 -2108
  42. package/dist/standalone/wave3d.standalone.webgpu.js +7372 -5848
  43. package/dist/standalone.d.ts +2 -2
  44. package/dist/standalone.js +2 -2
  45. package/dist/studio/StudioWaveRenderer.d.ts +115 -8
  46. package/dist/studio/StudioWaveRenderer.js +588 -14
  47. package/dist/studio/StudioWaveRenderer.js.map +1 -1
  48. package/dist/studio/index.d.ts +2 -2
  49. package/dist/studio/index.js.map +1 -1
  50. package/dist/studio/randomize.js +0 -1
  51. package/dist/studio/randomize.js.map +1 -1
  52. package/package.json +1 -1
  53. package/skills/wave3d/SKILL.md +142 -5
@@ -40,6 +40,18 @@ declare const CAMERA_FITS: readonly CameraFit[];
40
40
  /** What fills the 2D palette texture: the baked hero LUT, our editable stops, or
41
41
  * a named built-in map (see PALETTE_MAPS). Any string is allowed for forward-compat. */
42
42
  type PaletteSource = "hero" | "stops" | (string & {});
43
+ /**
44
+ * One control point of a {@link WaveConfig.path}: where the ribbon's centre passes, how wide it is
45
+ * there, and how far its cross-section has rotated. `width` 1 is the ribbon's natural width; `twist`
46
+ * is in degrees. Both are optional and default to 1 / 0.
47
+ */
48
+ interface PathPoint {
49
+ x: number;
50
+ y: number;
51
+ z: number;
52
+ width?: number;
53
+ twist?: number;
54
+ }
43
55
  /** A positionable light. `position` lives in the same 3D space as the wave. */
44
56
  interface LightConfig {
45
57
  position: Vec3;
@@ -103,6 +115,8 @@ declare function createDefaultMeshPoints(): MeshGradientPoint[];
103
115
  * (normalizeWaveColour, randomize*) map 1:1.
104
116
  */
105
117
  interface WaveConfig {
118
+ /** What to call this wave in the studio, if "Wave 3" is not enough. Absent ⇒ that numbering. */
119
+ name?: string;
106
120
  palette: ColorStop[];
107
121
  gradientType: GradientType;
108
122
  gradientAngle: number;
@@ -139,7 +153,8 @@ interface WaveConfig {
139
153
  edgeFade: number;
140
154
  /** Softness of the ribbon's two ENDS — it smoothsteps on uv.y, which is the length, not the
141
155
  * long edges. 0.1 = the original hardcoded value; smaller = razor-crisp graphic ribbons,
142
- * larger = soft vapor. */
156
+ * larger = soft vapor. Both themes honour it: on the wireframe it is what stops a sweep ending
157
+ * at a flat end-cap that reads as a straight cut drawn across the strands. */
143
158
  edgeFeather: number;
144
159
  /** Depth tint (solid theme): fade far fragments toward depthTintColor for atmospheric
145
160
  * separation in multi-wave stacks (0 = off). */
@@ -181,17 +196,143 @@ interface WaveConfig {
181
196
  radialSpread?: number;
182
197
  radialRadius?: number;
183
198
  radialCenter?: number;
184
- theme?: "solid" | "wireframe";
199
+ /** Lift the fan out of its own plane as it spreads, turning the flat plume into a CONE — a
200
+ * trumpet / morning-glory mouth whose combed strands run down the slant into the throat, which is
201
+ * the one thing neither the twists nor the helix can reach (a helix carries the ribbon around an
202
+ * axis, but its WIDTH never follows the slant). Measured as lift per ribbon-length of radius:
203
+ * 0 = the flat fan (the default, byte-identical), ~0.6 a wide mouth, ~1.4 a narrow horn.
204
+ * Negative cones the other way. Inert unless `radialAmount` > 0. */
205
+ radialCone?: number;
206
+ /** Degrees of ANGLE the band gains along its own length. The fan's angle otherwise comes from uv.x
207
+ * alone, so every arm runs straight out from the throat; radius already grows with uv.y, and
208
+ * letting angle grow with it too is exactly what turns a straight arm into a SPIRAL one that
209
+ * curves around the throat. 0 = straight (the default, byte-identical); 150 wraps most of a
210
+ * half-turn. Negative spirals the other way. Inert unless `radialAmount` > 0. */
211
+ radialSwirl?: number;
212
+ /**
213
+ * PATH — the ribbon's centreline, as control points it is swept along. Absent ⇒ the straight
214
+ * centreline the folded geometry is born with (byte-identical: the shader block is not compiled).
215
+ *
216
+ * This is the shape control the others cannot substitute for. The twists rotate a ribbon whose
217
+ * centreline is fixed, the helix carries that fixed centreline around an axis, the radial fan
218
+ * splays it — so none of them can make a ribbon that changes direction more than once, crosses
219
+ * itself, or is wide here and narrow there. A path can, because it IS the centreline.
220
+ *
221
+ * Points are in the wave's LOCAL space, the same units the geometry uses: the un-pathed ribbon
222
+ * runs from x −200 to +200 along its length, so `straightPath()` reproduces it. They are swept by
223
+ * ARC LENGTH with a parallel-transported frame, which is what keeps the strand comb even however
224
+ * the points are dragged and stops the ribbon snapping through inflections.
225
+ *
226
+ * Per point, `width` scales the ribbon's width there (0.1 is a throat, 2 a flare — this is what a
227
+ * separate "pinch" knob would otherwise be) and `twist` rotates its cross-section in degrees.
228
+ * Both interpolate smoothly between points.
229
+ */
230
+ path?: PathPoint[];
231
+ theme?: "solid" | "wireframe" | "glass";
232
+ /** Glass only: how far the ribbon bends what is behind it, in PIXELS at the silhouette. The bend
233
+ * is strongest where the surface turns away from the camera and falls to nothing face-on, which
234
+ * is what gives a sheet its edge compression. 0 is a clear pane. */
235
+ glassStrength?: number;
236
+ /** Glass only: per-channel split of that bend (dispersion). Past ~1 it reads as an oil sheen. */
237
+ glassChroma?: number;
238
+ /** Glass only: 0 clear · 1 frosted. Blurs the backdrop AT the refracted position, so the frost
239
+ * rides the bend rather than sitting flat under it. The scatter radius grows with the square of
240
+ * this, and the taps are skipped while it is under half a pixel (below ~0.1), where they could
241
+ * only average back to the sample they surround. */
242
+ glassFrost?: number;
243
+ /** Glass only: strength of the edge glint. It ADDS light over a dark backdrop and DARKENS over a
244
+ * bright one, which is what keeps a rim visible on white paper. */
245
+ glassSpec?: number;
246
+ /** Glass only: how far the interior pulls toward mid-grey — legibility for anything read through
247
+ * the sheet, and the haze that separates glass from a clear hole. */
248
+ glassVibrancy?: number;
249
+ /** Glass only: how much of the wave's own palette colour tints the glass (0 = colourless).
250
+ * Deliberately LOW by default. Glass reads as glass because of what is behind it being bent, not
251
+ * because the sheet carries colour — push this up and it stops looking like glass and starts
252
+ * looking like a filled material that happens to be shiny. */
253
+ glassTint?: number;
254
+ /** Glass only: HALF the optical path at normal incidence — the sheet's thickness. With density it
255
+ * sets how saturated the transmitted colour gets. This is what makes glass a material rather
256
+ * than a window: the colour comes from the ribbon's own palette absorbed over its own thickness,
257
+ * so it reads as glass with nothing behind it at all. */
258
+ glassPath?: number;
259
+ /** Glass only: absorption coefficient. High = deep, saturated glass; low = barely tinted. */
260
+ glassDensity?: number;
261
+ /** Glass only: edge whitening. The band is deliberately WIDE — a narrow one is thinner than a
262
+ * pixel on a thin ribbon and the knob does nothing at all. */
263
+ glassRim?: number;
264
+ /** Glass only: thin-film iridescence on the reflection, rim and specular — never on the
265
+ * transmission, which would read as dye rather than as a film. */
266
+ glassIrid?: number;
267
+ /** Glass only: optical film thickness in nm; 300–500 is the soap-bubble band. */
268
+ glassFilmNm?: number;
269
+ /** Glass only: index of refraction, used by the fresnel and the film. */
270
+ glassIor?: number;
271
+ /** Glass only: how much a fold over itself thickens the sheet. Glass draws opaque, so the layer
272
+ * behind is otherwise invisible and a doubled-back ribbon looks exactly as thin as a single
273
+ * sheet — this is the cue that reads as volume. 0 ignores overlap entirely. */
274
+ glassLayerGain?: number;
275
+ /** Glass only: DROPLET FUSION. Takes the refraction's direction from the gradient of the merged
276
+ * silhouette rather than from each surface's own normal, so two sheets passing close read as one
277
+ * blob of something viscous — in the neck between them the bend rotates smoothly from one rim to
278
+ * the other instead of tearing between two centres. 0 is off and each sheet keeps its own. */
279
+ glassFusion?: number;
280
+ /** Glass only: CAUSTICS — brightness where the refraction map compresses and neighbouring rays
281
+ * pile up, darkness where it spreads. Computed from the Jacobian of the sampling map, so it
282
+ * costs no extra pass and lands exactly where the optics put it rather than being painted on.
283
+ * Only visible where there is a backdrop to concentrate: a sheet over blank page has no light
284
+ * to gather. */
285
+ glassCaustic?: number;
286
+ /** Glass only: LIQUID — how hard four travelling waves tilt the surface normal. Everything
287
+ * downstream (dispersion, rim, specular) reads the rippled normal, so the shimmer stays coherent
288
+ * instead of sitting on top as a separate layer. 0 is still glass, just not moving. */
289
+ glassRipple?: number;
290
+ /** Glass only: waves per world unit. */
291
+ glassRippleScale?: number;
292
+ /** Glass only: how fast they travel, rad/s. */
293
+ glassFlow?: number;
294
+ /** Glass only: falloff exponent of the rim band. Low = the whole sheet bends; high = only the
295
+ * silhouette does, which is the crisp compression ring. */
296
+ glassRimPower?: number;
185
297
  lineAmount?: number;
186
298
  lineThickness?: number;
187
299
  lineDerivativePower?: number;
300
+ /** Wireframe only: how hard the strands recede INTO the page background with depth. 1 (the
301
+ * default) is the original hardcoded fade — it gives a single ribbon its sense of depth, but on a
302
+ * tightly-fitted near/far slab it washes out the whole back half of a deep or stacked
303
+ * composition. 0 turns it off, so every strand holds full contrast wherever it sits: the flat,
304
+ * graphic, poster look. */
305
+ lineDepthFade?: number;
306
+ /** Wireframe only: 0..1, how HARD the edge of each strand is. The stripe is a soft ramp by
307
+ * default (0), which means {@link lineThickness} widens the strands by fading the gaps away with
308
+ * them — the surface goes from pale hairlines to flat solid without passing through dense ink.
309
+ * Raising this steepens the ramp about its midpoint, which splits the two controls apart:
310
+ * `lineThickness` becomes the DUTY CYCLE (how much of each period is strand rather than gap) and
311
+ * this becomes the edge. 0.9 with `lineThickness` ~1.5 is heavy ink with crisp gaps still
312
+ * reading — the engraved / guilloché look. Default 0 (the original soft ramp). */
313
+ lineSharpness?: number;
314
+ /** Wireframe only: what sits between the strands. ABSENT = the page background, which makes the
315
+ * wave a window onto the page (dark strands, paper showing through) — the theme as it has always
316
+ * drawn. A colour makes the ribbon its own BODY: a dark gap colour under a bright palette is an
317
+ * opaque striped surface, which is the other half of this theme's range and the one that reads as
318
+ * a lit solid rather than a drawing. `"transparent"` (or an 8-digit hex with a low alpha) leaves
319
+ * the gaps CLEAR instead, so stacked folds show through each other rather than occluding —
320
+ * airier, but the near fold no longer hides the far one, which is what makes a stack read solid. */
321
+ lineGapColor?: string;
322
+ /** Wireframe only: 0..1, how much the strands are LIT. The line theme is otherwise unlit — a
323
+ * strand's colour comes from its uv alone, so it holds one tone wherever the surface turns, which
324
+ * is why a dense wireframe reads as a printed pattern instead of an object. Turn this up and the
325
+ * same derivative normal and scene `lights` the solid theme uses shade it, AND each strand is
326
+ * given a round cross-section, so a specular runs along one strand and not its neighbour. (Those
327
+ * are one knob on purpose: shading a flat stripe barely reads — it is the round section that makes
328
+ * a strand look like a filament.) Default 0. */
329
+ lineLight?: number;
188
330
  /** Wireframe RUNGS: a second line family carved at constant uv.y, so these run ACROSS the ribbon
189
331
  * where `lineAmount`'s run along it — the two cross into a ladder. Frequency, like `lineAmount`
190
332
  * (rungs ≈ amount / π). 0 = off, and the cross-wise path isn't compiled. */
191
333
  rungAmount?: number;
192
334
  /** Rung line width, in pixels (screen-space, so it holds at any zoom). */
193
335
  rungThickness?: number;
194
- maxWidth?: number;
195
336
  position: Vec3;
196
337
  rotation: Vec3;
197
338
  scale: Vec3;
@@ -208,6 +349,53 @@ interface WaveConfig {
208
349
  /** Optional per-wave particle / dust field emitted off THIS wave's deformed surface / edge.
209
350
  * ABSENT ⇒ off (no THREE.Points for this wave, byte-identical). See {@link ParticlesConfig}. */
210
351
  particles?: ParticlesConfig;
352
+ /** Optional disintegration ("the snap"): a front that sweeps across the ribbon eating it away in
353
+ * chunks, and — with a particle field — blowing those chunks off as dust. ABSENT ⇒ intact (no
354
+ * DISSOLVE program, byte-identical). See {@link DissolveConfig}. */
355
+ dissolve?: DissolveConfig;
356
+ }
357
+ /** Which way a {@link DissolveConfig} front sweeps. `length` / `width` follow the RIBBON's own uv
358
+ * axes, so the front travels with the sheet wherever the twist takes it; `screenX` / `screenY` are
359
+ * a straight line on the CANVAS, so every wave in a stack crumbles against the same edge whatever
360
+ * each one's orientation. */
361
+ type DissolveAxis = "length" | "width" | "screenX" | "screenY";
362
+ declare const DISSOLVE_AXES: readonly DissolveAxis[];
363
+ /**
364
+ * DISINTEGRATION — the "snap". A band sweeps across the ribbon in uv and everything behind it is
365
+ * eaten away chunk by chunk, so the surface CRUMBLES rather than fading: holes open in it, the holes
366
+ * merge, and the last fragments break off. Where the wave also has {@link ParticlesConfig}, `dust`
367
+ * pins that field to the same front, so the motes are the chunks that just left — the surface does
368
+ * not fade into an unrelated cloud, it becomes one.
369
+ *
370
+ * `amount` is the whole animation: 0 is intact and 1 is gone, whatever the band width, so binding it
371
+ * to `scroll` (or any other input — it is a {@link WaveInteractionTarget}) disintegrates the wave as
372
+ * the reader moves. Absent ⇒ the DISSOLVE shader path is never compiled.
373
+ */
374
+ interface DissolveConfig {
375
+ /** 0..1 — how far the front has swept. 0 = the ribbon is whole; 1 = every chunk is gone. */
376
+ amount: number;
377
+ /** Which way the front travels. `"length"` (uv.y, end to end — the default) and `"width"` (uv.x,
378
+ * across the folded cross-section) ride the ribbon, so the front bends with it; mind the axes —
379
+ * uv.y is the LENGTH (see the UV AXES note atop WaveGeometry). `"screenX"` / `"screenY"` sweep a
380
+ * straight line across the CANVAS instead, which is what makes a multi-wave composition crumble
381
+ * as ONE object: give every wave the same axis and amount and they share one edge. The crumb
382
+ * pattern stays on the surface either way. */
383
+ axis?: DissolveAxis;
384
+ /** Sweep from the far end instead of the near one. */
385
+ reverse?: boolean;
386
+ /** Width of the crumbling band, in uv. 0.05 is a clean guillotine edge; 0.6 is a long ragged fray
387
+ * where half the ribbon is mid-flight at once. Default 0.35. */
388
+ band?: number;
389
+ /** How finely the ribbon is diced — chunks across its WIDTH (they are kept square on the sheet,
390
+ * so the length gets ~2.1× as many). Default 90; smaller = big slabs, larger = fine grit. */
391
+ scale?: number;
392
+ /** 0..1 — chunk character: 0 = organic torn tatters (smooth noise), 1 = hard quantized cells
393
+ * (blocky pixel debris). Default 0.6. */
394
+ blocky?: number;
395
+ /** 0..1 — how strongly this wave's own dust is pinned to the front: 1 = each mote peels off
396
+ * exactly where and when the surface under it crumbles and drifts on from there; 0 = the field
397
+ * free-runs on `life` as it always has. Default 1. Inert without {@link WaveConfig.particles}. */
398
+ dust?: number;
211
399
  }
212
400
  /**
213
401
  * An interaction INPUT: a normalized signal that can smoothly drive config params through an
@@ -216,7 +404,7 @@ interface WaveConfig {
216
404
  type InteractionSource = "scroll" | "hover" | "pointerX" | "pointerY" | "pointerSpeed" | "press" | "scrollVelocity" | "appear" | "tiltX" | "tiltY" | `custom:${string}`;
217
405
  /** Per-WAVE params a binding may drive. Single source of truth for WAVE_APPLIERS in
218
406
  * renderer/interaction.ts (checked via `satisfies`) and validated by normalizeWaveInteraction. */
219
- declare const WAVE_TARGET_NAMES: readonly ["displaceAmount", "detailAmount", "twistPowerX", "twistPowerY", "twistPowerZ", "twistFrequencyX", "twistFrequencyY", "twistFrequencyZ", "helixPhase", "helixTurns", "helixRadius", "hueShift", "gradientShift", "colorSaturation", "opacity", "lineThickness", "lineAmount", "fiberStrength", "sheen", "iridescence", "positionX", "positionY"];
407
+ declare const WAVE_TARGET_NAMES: readonly ["displaceAmount", "detailAmount", "twistPowerX", "twistPowerY", "twistPowerZ", "twistFrequencyX", "twistFrequencyY", "twistFrequencyZ", "helixPhase", "helixTurns", "helixRadius", "dissolveAmount", "glassRipple", "glassStrength", "glassTint", "hueShift", "gradientShift", "colorSaturation", "opacity", "lineThickness", "lineAmount", "fiberStrength", "sheen", "iridescence", "positionX", "positionY"];
220
408
  /** A per-wave param a {@link WaveInteractionBinding} can drive. */
221
409
  type WaveInteractionTarget = (typeof WAVE_TARGET_NAMES)[number];
222
410
  /** SCENE params a binding may drive (post / camera / time — shared, not per wave). Single source of
@@ -358,7 +546,7 @@ interface TiltConfig {
358
546
  /** How each particle sprite is drawn (a per-field render style, not per-particle). All but
359
547
  * "sprite" are drawn procedurally from `gl_PointCoord`; "sprite" samples {@link
360
548
  * ParticlesConfig.spriteUrl} and falls back to "glitter" until that image has rasterized. */
361
- type ParticleShape = "glitter" | "soft" | "ring" | "star" | "streak" | "sprite";
549
+ type ParticleShape = "glitter" | "soft" | "ring" | "star" | "streak" | "square" | "sprite";
362
550
  declare const PARTICLE_SHAPES: readonly ParticleShape[];
363
551
  interface ParticlesConfig {
364
552
  count: number;
@@ -388,6 +576,14 @@ interface ParticlesConfig {
388
576
  wander?: number;
389
577
  /** Sprite render style. Default "glitter" (the soft round additive disc). */
390
578
  shape?: ParticleShape;
579
+ /**
580
+ * How the sprites composite. `"additive"` (the default) ADDS light — glints, embers, sparks; it can
581
+ * only ever brighten, so additive dust is invisible on a white page and can never read as dark.
582
+ * `"normal"` alpha-blends them instead, which is what a dark mote on a light ground needs: soot,
583
+ * ash, ink, the blocky debris a {@link DissolveConfig} sheds across a pale background. Either way
584
+ * the field never writes depth, so it composites over the waves rather than occluding them.
585
+ */
586
+ blend?: "additive" | "normal";
391
587
  /**
392
588
  * Artwork for `shape: "sprite"` — an SVG (or raster) `data:` URI or URL, rasterized ONCE into a
393
589
  * square texture shared by every particle in the field, so the cost is one texture per field and
@@ -557,10 +753,17 @@ declare function normalizeSceneInteraction(config: StudioConfig): void;
557
753
  * when the block is absent — absence is off and byte-identical (ensureStudioConfig gates on presence).
558
754
  */
559
755
  declare function normalizeParticles(wave: WaveConfig): void;
756
+ /** Clamp a present {@link WaveConfig.path}: drop anything that is not a finite point, and drop the
757
+ * whole path if fewer than two survive (one point is not a centreline). Present-only, like the
758
+ * particle and dissolve blocks — absence means "the straight ribbon". */
759
+ declare function normalizePath(wave: WaveConfig): void;
760
+ /** Clamp a present {@link DissolveConfig} (present-only, like {@link normalizeParticles}: a wave
761
+ * with no `dissolve` block is left exactly as it is). */
762
+ declare function normalizeDissolve(wave: WaveConfig): void;
560
763
  /** Normalize an ingested config to the wave model: backfill the scene + every wave, and drop in
561
764
  * a default wave if none are present. Idempotent, so it is safe on the renderer's own config as
562
765
  * well as freshly loaded save-states / share links. */
563
766
  declare function ensureStudioConfig(input: StudioConfig): StudioConfig;
564
767
  //#endregion
565
- export { 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 };
768
+ export { 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 };
566
769
  //# sourceMappingURL=model.d.ts.map
@@ -96,6 +96,12 @@ function createDefaultMeshPoints() {
96
96
  }
97
97
  ];
98
98
  }
99
+ const DISSOLVE_AXES = [
100
+ "length",
101
+ "width",
102
+ "screenX",
103
+ "screenY"
104
+ ];
99
105
  /** The built-in interaction input names (the open-ended `custom:*` family is handled separately).
100
106
  * Kept in sync by hand with the {@link InteractionSource} union below. */
101
107
  const INTERACTION_SOURCE_NAMES = [
@@ -124,6 +130,10 @@ const WAVE_TARGET_NAMES = [
124
130
  "helixPhase",
125
131
  "helixTurns",
126
132
  "helixRadius",
133
+ "dissolveAmount",
134
+ "glassRipple",
135
+ "glassStrength",
136
+ "glassTint",
127
137
  "hueShift",
128
138
  "gradientShift",
129
139
  "colorSaturation",
@@ -150,6 +160,7 @@ const PARTICLE_SHAPES = [
150
160
  "ring",
151
161
  "star",
152
162
  "streak",
163
+ "square",
153
164
  "sprite"
154
165
  ];
155
166
  /** Spread a base wave into `count` overlapping waves — each with a slightly varied hue, width,
@@ -285,13 +296,17 @@ function defaultWave() {
285
296
  radialSpread: 1,
286
297
  radialRadius: 40,
287
298
  radialCenter: 0,
299
+ radialCone: 0,
300
+ radialSwirl: 0,
288
301
  theme: "solid",
289
302
  lineAmount: 425,
290
303
  lineThickness: 1,
291
304
  lineDerivativePower: .95,
305
+ lineDepthFade: 1,
306
+ lineSharpness: 0,
307
+ lineLight: 0,
292
308
  rungAmount: 0,
293
309
  rungThickness: 1,
294
- maxWidth: 1232,
295
310
  position: {
296
311
  x: -24.3,
297
312
  y: -56.4,
@@ -542,13 +557,39 @@ function normalizeWave(s) {
542
557
  if (!Number.isFinite(s.radialSpread)) s.radialSpread = 1;
543
558
  if (!Number.isFinite(s.radialRadius)) s.radialRadius = 40;
544
559
  if (!Number.isFinite(s.radialCenter)) s.radialCenter = 0;
560
+ if (!Number.isFinite(s.radialCone)) s.radialCone = 0;
561
+ if (!Number.isFinite(s.radialSwirl)) s.radialSwirl = 0;
545
562
  if (typeof s.theme !== "string") s.theme = "solid";
563
+ if (s.theme === "glass") {
564
+ if (!Number.isFinite(s.glassStrength)) s.glassStrength = 90;
565
+ if (!Number.isFinite(s.glassChroma)) s.glassChroma = .7;
566
+ if (!Number.isFinite(s.glassFrost)) s.glassFrost = .08;
567
+ if (!Number.isFinite(s.glassSpec)) s.glassSpec = 1.2;
568
+ if (!Number.isFinite(s.glassVibrancy)) s.glassVibrancy = .05;
569
+ if (!Number.isFinite(s.glassTint)) s.glassTint = .12;
570
+ if (!Number.isFinite(s.glassRimPower)) s.glassRimPower = 1.2;
571
+ if (!Number.isFinite(s.glassRipple)) s.glassRipple = 0;
572
+ if (!Number.isFinite(s.glassRippleScale)) s.glassRippleScale = .012;
573
+ if (!Number.isFinite(s.glassFlow)) s.glassFlow = .9;
574
+ if (!Number.isFinite(s.glassPath)) s.glassPath = .45;
575
+ if (!Number.isFinite(s.glassDensity)) s.glassDensity = 1.2;
576
+ if (!Number.isFinite(s.glassRim)) s.glassRim = .5;
577
+ if (!Number.isFinite(s.glassIrid)) s.glassIrid = 0;
578
+ if (!Number.isFinite(s.glassFilmNm)) s.glassFilmNm = 380;
579
+ if (!Number.isFinite(s.glassIor)) s.glassIor = 1.45;
580
+ if (!Number.isFinite(s.glassLayerGain)) s.glassLayerGain = .6;
581
+ if (!Number.isFinite(s.glassFusion)) s.glassFusion = 0;
582
+ if (!Number.isFinite(s.glassCaustic)) s.glassCaustic = .4;
583
+ }
546
584
  if (!Number.isFinite(s.lineAmount)) s.lineAmount = 425;
547
585
  if (!Number.isFinite(s.lineThickness)) s.lineThickness = 1;
548
586
  if (!Number.isFinite(s.lineDerivativePower)) s.lineDerivativePower = .95;
587
+ if (!Number.isFinite(s.lineDepthFade)) s.lineDepthFade = 1;
588
+ if (!Number.isFinite(s.lineSharpness)) s.lineSharpness = 0;
589
+ if (s.lineGapColor !== void 0 && typeof s.lineGapColor !== "string") delete s.lineGapColor;
590
+ if (!Number.isFinite(s.lineLight)) s.lineLight = 0;
549
591
  if (!Number.isFinite(s.rungAmount)) s.rungAmount = 0;
550
592
  if (!Number.isFinite(s.rungThickness)) s.rungThickness = 1;
551
- if (!Number.isFinite(s.maxWidth)) s.maxWidth = 1232;
552
593
  if (!s.position) s.position = {
553
594
  x: 0,
554
595
  y: 0,
@@ -570,6 +611,13 @@ function normalizeWave(s) {
570
611
  if (!Number.isFinite(s.seed)) s.seed = 0;
571
612
  if (s.interaction) normalizeWaveInteraction(s);
572
613
  if (s.particles) normalizeParticles(s);
614
+ if (s.dissolve) normalizeDissolve(s);
615
+ if (typeof s.name === "string") {
616
+ const named = s.name.trim().slice(0, 60);
617
+ if (named) s.name = named;
618
+ else delete s.name;
619
+ } else if (s.name !== void 0) delete s.name;
620
+ if (s.path) normalizePath(s);
573
621
  }
574
622
  /** Backfill scene-level defaults (background/camera/post/lights/quality/mirror). */
575
623
  function ensureSceneDefaults(config) {
@@ -743,9 +791,43 @@ function normalizeParticles(wave) {
743
791
  if (p.swirl !== void 0) p.swirl = num(p.swirl, 0);
744
792
  if (p.wander !== void 0) p.wander = num(p.wander, 0);
745
793
  if (p.shape !== void 0 && !PARTICLE_SHAPES.includes(p.shape)) p.shape = "glitter";
794
+ if (p.blend !== void 0 && p.blend !== "additive" && p.blend !== "normal") p.blend = "additive";
746
795
  if (p.spriteUrl !== void 0 && typeof p.spriteUrl !== "string") delete p.spriteUrl;
747
796
  if (p.pointerShove !== void 0) p.pointerShove = clampNumber(p.pointerShove, 0, 4, 1);
748
797
  }
798
+ /** Clamp a present {@link WaveConfig.path}: drop anything that is not a finite point, and drop the
799
+ * whole path if fewer than two survive (one point is not a centreline). Present-only, like the
800
+ * particle and dissolve blocks — absence means "the straight ribbon". */
801
+ function normalizePath(wave) {
802
+ const p = wave.path;
803
+ if (!p) return;
804
+ if (!Array.isArray(p)) {
805
+ delete wave.path;
806
+ return;
807
+ }
808
+ const pts = p.filter((q) => !!q && typeof q === "object").map((q) => ({
809
+ x: num(q.x, 0),
810
+ y: num(q.y, 0),
811
+ z: num(q.z, 0),
812
+ width: q.width === void 0 ? void 0 : clampNumber(q.width, 0, 8, 1),
813
+ twist: q.twist === void 0 ? void 0 : num(q.twist, 0)
814
+ })).filter((q) => Number.isFinite(q.x) && Number.isFinite(q.y) && Number.isFinite(q.z));
815
+ if (pts.length < 2) delete wave.path;
816
+ else wave.path = pts;
817
+ }
818
+ /** Clamp a present {@link DissolveConfig} (present-only, like {@link normalizeParticles}: a wave
819
+ * with no `dissolve` block is left exactly as it is). */
820
+ function normalizeDissolve(wave) {
821
+ const d = wave.dissolve;
822
+ if (!d) return;
823
+ d.amount = clampNumber(d.amount, 0, 1, 0);
824
+ if (d.axis !== void 0 && !DISSOLVE_AXES.includes(d.axis)) d.axis = "length";
825
+ if (d.reverse !== void 0) d.reverse = !!d.reverse;
826
+ if (d.band !== void 0) d.band = clampNumber(d.band, .01, 1, .35);
827
+ if (d.scale !== void 0) d.scale = clampNumber(d.scale, 2, 600, 90);
828
+ if (d.blocky !== void 0) d.blocky = clampNumber(d.blocky, 0, 1, .6);
829
+ if (d.dust !== void 0) d.dust = clampNumber(d.dust, 0, 1, 1);
830
+ }
749
831
  /** Normalize an ingested config to the wave model: backfill the scene + every wave, and drop in
750
832
  * a default wave if none are present. Idempotent, so it is safe on the renderer's own config as
751
833
  * well as freshly loaded save-states / share links. */
@@ -759,6 +841,6 @@ function ensureStudioConfig(input) {
759
841
  return config;
760
842
  }
761
843
  //#endregion
762
- export { 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 };
844
+ 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, ensureCamera, ensureSceneDefaults, ensureStudioConfig, makeStops, makeWave, makeWaveSpread, normalizeBackground, normalizeDissolve, normalizeParticles, normalizePath, normalizeSceneInteraction, normalizeWave, normalizeWaveInteraction, resizeWaves };
763
845
 
764
846
  //# sourceMappingURL=model.js.map