@pixerate/flair 0.28.0 → 0.29.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 (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +48 -6
  3. package/dist/catalog/index.d.ts +11 -6
  4. package/dist/catalog/index.js +1 -1
  5. package/dist/{chunk-4LKAPRFM.js → chunk-6GFOLHCT.js} +920 -34
  6. package/dist/chunk-6GFOLHCT.js.map +1 -0
  7. package/dist/{chunk-JL6TI6II.js → chunk-7KYQ4R76.js} +2 -2
  8. package/dist/chunk-7KYQ4R76.js.map +1 -0
  9. package/dist/{chunk-STKEKXCK.js → chunk-AUH3PEFW.js} +2 -2
  10. package/dist/{chunk-PFGGPIMT.js → chunk-DA4UUSDO.js} +58 -3
  11. package/dist/chunk-DA4UUSDO.js.map +1 -0
  12. package/dist/{chunk-RDU63CDO.js → chunk-HDYBY3PQ.js} +380 -1
  13. package/dist/chunk-HDYBY3PQ.js.map +1 -0
  14. package/dist/{chunk-VLQLRKGW.js → chunk-KZYZX3V5.js} +260 -33
  15. package/dist/chunk-KZYZX3V5.js.map +1 -0
  16. package/dist/{chunk-GXP4YLAP.js → chunk-MBKNTXQX.js} +9 -9
  17. package/dist/chunk-MBKNTXQX.js.map +1 -0
  18. package/dist/{chunk-H6VDFBIO.js → chunk-SMR2MJXB.js} +156 -5
  19. package/dist/chunk-SMR2MJXB.js.map +1 -0
  20. package/dist/{chunk-YBGRXVAE.js → chunk-X3C2VFTM.js} +3 -3
  21. package/dist/{chunk-CK6EV37I.js → chunk-ZULFHADW.js} +69 -6
  22. package/dist/chunk-ZULFHADW.js.map +1 -0
  23. package/dist/cli.js +6 -6
  24. package/dist/editor/index.d.ts +3 -3
  25. package/dist/editor/index.js +3 -3
  26. package/dist/{effect-GGP66JI6.js → effect-YBWDNIYG.js} +10 -10
  27. package/dist/embed/index.d.ts +36 -8
  28. package/dist/embed/index.js +5 -5
  29. package/dist/{frames-VEcqa40g.d.ts → frames-CGJudAbu.d.ts} +11 -1
  30. package/dist/index.d.ts +69 -6
  31. package/dist/index.js +35 -7
  32. package/dist/index.js.map +1 -1
  33. package/dist/node.d.ts +4 -4
  34. package/dist/node.js +9 -9
  35. package/dist/react/index.d.ts +4 -4
  36. package/dist/react/index.js +7 -7
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/{recorder-CDPPTJMG.js → recorder-TYMEZHOT.js} +3 -3
  39. package/dist/{renderer-teaa98RE.d.ts → renderer-tk1nbVkW.d.ts} +252 -2
  40. package/dist/svelte/index.d.ts +4 -4
  41. package/dist/svelte/index.js +7 -7
  42. package/dist/svelte/index.js.map +1 -1
  43. package/dist/{svg-BhRboN9q.d.ts → svg-DaKRPA9v.d.ts} +39 -7
  44. package/dist/{types-DUOMxZaq.d.ts → types-wh28fXW4.d.ts} +2 -2
  45. package/dist/vector/index.d.ts +2 -2
  46. package/dist/vector/index.js +5 -3
  47. package/package.json +1 -1
  48. package/dist/chunk-4LKAPRFM.js.map +0 -1
  49. package/dist/chunk-CK6EV37I.js.map +0 -1
  50. package/dist/chunk-GXP4YLAP.js.map +0 -1
  51. package/dist/chunk-H6VDFBIO.js.map +0 -1
  52. package/dist/chunk-JL6TI6II.js.map +0 -1
  53. package/dist/chunk-PFGGPIMT.js.map +0 -1
  54. package/dist/chunk-RDU63CDO.js.map +0 -1
  55. package/dist/chunk-VLQLRKGW.js.map +0 -1
  56. /package/dist/{chunk-STKEKXCK.js.map → chunk-AUH3PEFW.js.map} +0 -0
  57. /package/dist/{chunk-YBGRXVAE.js.map → chunk-X3C2VFTM.js.map} +0 -0
  58. /package/dist/{effect-GGP66JI6.js.map → effect-YBWDNIYG.js.map} +0 -0
  59. /package/dist/{recorder-CDPPTJMG.js.map → recorder-TYMEZHOT.js.map} +0 -0
@@ -15,8 +15,8 @@ import {
15
15
  } from "./chunk-ORAFCHX3.js";
16
16
 
17
17
  // src/catalog/inputs.ts
18
- var EFFECT_INPUTS = ["image", "video", "svg", "text", "mask", "path"];
19
- var EFFECT_INTERACTIONS = ["hover", "press", "click", "trail", "pointer"];
18
+ var EFFECT_INPUTS = ["image", "video", "svg", "text", "mask", "path", "audio"];
19
+ var EFFECT_INTERACTIONS = ["hover", "press", "click", "trail", "pointer", "scroll"];
20
20
  function isTextImage(url) {
21
21
  if (!url.startsWith("data:image/svg+xml;charset=utf-8,")) return false;
22
22
  try {
@@ -31,14 +31,16 @@ function textSlot(control) {
31
31
  const holds2 = control.name === "font" || /\bfont\b/i.test(control.label) ? "font" : PATH_DATA.test(value) ? "path" : control.name === "glyphs" ? "glyphs" : "copy";
32
32
  return { name: control.name, label: control.label, holds: holds2, default: value };
33
33
  }
34
+ var codeOf = (source) => source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "");
34
35
  function shaderInteractions(source) {
35
- const code = source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "");
36
+ const code = codeOf(source);
36
37
  const found = /* @__PURE__ */ new Set();
37
38
  if (/\biClicks\b/.test(code)) found.add("click");
38
39
  if (/\biTrail\b/.test(code)) found.add("trail");
39
40
  if (/\biPointer\s*\.\s*z\b/.test(code)) found.add("hover");
40
41
  if (/\biPointer\s*\.\s*w\b/.test(code) || /\biMouse\s*\.\s*z\b/.test(code)) found.add("press");
41
42
  if (/\biPointer\s*\.\s*xy?\b/.test(code) || /\biMouse\s*\.\s*xy?\b/.test(code)) found.add("pointer");
43
+ if (/\biScroll\b/.test(code)) found.add("scroll");
42
44
  return EFFECT_INTERACTIONS.filter((i) => found.has(i));
43
45
  }
44
46
  function inputsOf(effect, controls) {
@@ -64,6 +66,7 @@ function inputsOf(effect, controls) {
64
66
  if (slot.holds === "path") accepts.add("path");
65
67
  }
66
68
  }
69
+ if (effect.kind === "shader" && /\bi(Audio|Spectrum|Beat)\b/.test(codeOf(effect.source))) accepts.add("audio");
67
70
  const interactions = effect.kind === "vector" ? effect.trigger ? [effect.trigger] : [] : shaderInteractions(effect.source);
68
71
  return {
69
72
  accepts: EFFECT_INPUTS.filter((i) => accepts.has(i)),
@@ -4655,8 +4658,8 @@ function packGlyph(rows) {
4655
4658
  return `ivec2(${a}, ${b})`;
4656
4659
  }
4657
4660
  var vec3Hex = (hex) => {
4658
- const n11 = parseInt(hex.slice(1), 16);
4659
- const c = [n11 >> 16, n11 >> 8 & 255, n11 & 255].map((v) => +(v / 255).toFixed(3));
4661
+ const n12 = parseInt(hex.slice(1), 16);
4662
+ const c = [n12 >> 16, n12 >> 8 & 255, n12 & 255].map((v) => +(v / 255).toFixed(3));
4660
4663
  return `vec3(${c.map((v) => Number.isInteger(v) ? `${v}.0` : String(v)).join(", ")})`;
4661
4664
  };
4662
4665
  var paletteArray = (name, hexes) => `const vec3 ${name}[${hexes.length}] = vec3[${hexes.length}](
@@ -5714,7 +5717,7 @@ var chompLoader = {
5714
5717
  var pixelEffects = [pixelText, coinSpin, spaceInvader, pixelHeartHealth, chompLoader];
5715
5718
 
5716
5719
  // src/catalog/pyro.ts
5717
- var round = (n11) => Math.round(n11 * 10) / 10;
5720
+ var round = (n12) => Math.round(n12 * 10) / 10;
5718
5721
  var fireworkBurst = {
5719
5722
  kind: "vector",
5720
5723
  id: "firework-burst",
@@ -5769,7 +5772,7 @@ var fireworkBurst = {
5769
5772
  var pyroEffects = [fireworkBurst];
5770
5773
 
5771
5774
  // src/catalog/timewarp.ts
5772
- var r19 = (n11) => Math.round(n11 * 10) / 10;
5775
+ var r19 = (n12) => Math.round(n12 * 10) / 10;
5773
5776
  var at2 = (degrees, radius) => {
5774
5777
  const a = degrees * Math.PI / 180;
5775
5778
  return [r19(100 + radius * Math.sin(a)), r19(100 - radius * Math.cos(a))];
@@ -18439,26 +18442,26 @@ float coverage(vec2 p, float radius) {
18439
18442
  }
18440
18443
  return sum / 16.0;
18441
18444
  }`.trim();
18442
- function goldenTaps(n11) {
18443
- const taps = Array.from({ length: n11 }, (_, i) => {
18444
- const r = Math.sqrt((i + 0.5) / n11);
18445
+ function goldenTaps(n12) {
18446
+ const taps = Array.from({ length: n12 }, (_, i) => {
18447
+ const r = Math.sqrt((i + 0.5) / n12);
18445
18448
  const a = i * 2.39996;
18446
18449
  return `vec2(${(r * Math.cos(a)).toFixed(5)}, ${(r * Math.sin(a)).toFixed(5)})`;
18447
18450
  });
18448
- return `const vec2 GOLDEN${n11}[${n11}] = vec2[${n11}](${taps.join(", ")});`;
18451
+ return `const vec2 GOLDEN${n12}[${n12}] = vec2[${n12}](${taps.join(", ")});`;
18449
18452
  }
18450
- function coverageTaps(name, n11, blur) {
18453
+ function coverageTaps(name, n12, blur) {
18451
18454
  return `
18452
- ${goldenTaps(n11)}
18455
+ ${goldenTaps(n12)}
18453
18456
  float ${name}(vec2 p, float radius) {
18454
18457
  float sum = 0.0;
18455
18458
  if (gMaskPlace.w < 0.5) {
18456
- for (int i = 0; i < ${n11}; i++) sum += shapeAt(p + radius * GOLDEN${n11}[i], radius * ${blur.toFixed(2)});
18459
+ for (int i = 0; i < ${n12}; i++) sum += shapeAt(p + radius * GOLDEN${n12}[i], radius * ${blur.toFixed(2)});
18457
18460
  } else {
18458
18461
  float lod = log2(max(radius * ${blur.toFixed(2)} * gMaskPlace.z, 1.0));
18459
- for (int i = 0; i < ${n11}; i++) sum += shapeLod(p + radius * GOLDEN${n11}[i], lod);
18462
+ for (int i = 0; i < ${n12}; i++) sum += shapeLod(p + radius * GOLDEN${n12}[i], lod);
18460
18463
  }
18461
- return sum / ${n11}.0;
18464
+ return sum / ${n12}.0;
18462
18465
  }`.trim();
18463
18466
  }
18464
18467
  var LITE_TAPS = 10;
@@ -20624,6 +20627,885 @@ var lightingShaders = [
20624
20627
  partyLights
20625
20628
  ];
20626
20629
 
20630
+ // src/catalog/shaders/audio.ts
20631
+ var SOUND = `
20632
+ bool soundDemo() { return uDemo && iBeat == 0.0 && dot(iAudio, iAudio) == 0.0; }
20633
+ // The demo: a kick every half second, a hat between, and a gently wandering pink-ish spectrum.
20634
+ float demoKick() { return exp(-mod(iTime, 0.5) * 7.0); }
20635
+ float demoHat() { return exp(-mod(iTime + 0.25, 0.5) * 16.0); }
20636
+ vec4 sound() {
20637
+ if (!soundDemo()) return iAudio;
20638
+ float k = demoKick();
20639
+ float mid = 0.4 + 0.12 * sin(iTime * 1.7) + 0.1 * sin(iTime * 3.1);
20640
+ return vec4(0.45 + 0.3 * k + 0.1 * mid, 0.3 + 0.65 * k, mid, 0.25 + 0.4 * demoHat() + 0.08 * sin(iTime * 5.3));
20641
+ }
20642
+ float beat() { return soundDemo() ? mod(iTime, 0.5) : iBeat; }
20643
+ float spectrum(float x) {
20644
+ x = clamp(x, 0.0, 1.0);
20645
+ if (soundDemo()) {
20646
+ float tilt = 0.8 - 0.5 * x;
20647
+ float wander = 0.6 + 0.25 * sin(iTime * 0.7 + x * 5.0) + 0.1 * sin(x * 23.0 + iTime * 2.3) + 0.07 * sin(x * 47.0 - iTime * 3.7);
20648
+ return clamp(tilt * wander + 0.55 * demoKick() * exp(-x * 7.0) + 0.35 * demoHat() * smoothstep(0.55, 0.9, x), 0.0, 1.0);
20649
+ }
20650
+ float f = x * 63.0;
20651
+ int i = int(f);
20652
+ int j = min(i + 1, 63);
20653
+ return mix(iSpectrum[i / 4][i % 4], iSpectrum[j / 4][j % 4], f - float(i));
20654
+ }`.trim();
20655
+ var GLOW8 = `
20656
+ vec4 glow(vec3 c) {
20657
+ c = 1.0 - exp(-max(c, 0.0));
20658
+ float a = max(c.r, max(c.g, c.b));
20659
+ return vec4(c / max(a, 1e-4), a);
20660
+ }`.trim();
20661
+ var DEMO = `uniform bool uDemo; // @control default=true label="Demo when silent"`;
20662
+ var spectrumBars = {
20663
+ kind: "shader",
20664
+ id: "spectrum-bars",
20665
+ name: "Spectrum bars",
20666
+ description: "Classic equaliser bars dancing to the music, with peak caps kicked up on each beat and an optional mirrored layout.",
20667
+ category: "audio",
20668
+ tags: ["audio", "music", "equalizer", "spectrum", "bars", "visualizer", "beat"],
20669
+ source: glsl(
20670
+ `// Spectrum bars: the spectrum as bars, low to high, coloured by height, with caps that jump on beats.
20671
+ uniform int uBars; // @control min=8 max=64 default=32 label="Bars"
20672
+ uniform float uGap; // @control min=0 max=0.8 default=0.3 label="Gap"
20673
+ uniform float uGain; // @control min=0.3 max=2.5 default=1.1 label="Gain"
20674
+ uniform vec3 uColorLow; // @control color default=#22D3EE label="Low colour"
20675
+ uniform vec3 uColorHigh; // @control color default=#F43F5E label="High colour"
20676
+ uniform vec3 uBackground; // @control color default=#070B16 label="Background"
20677
+ uniform bool uMirror; // @control default=false label="Mirrored"
20678
+ uniform bool uPeaks; // @control default=true label="Peak caps"
20679
+ uniform float uRound; // @control min=0 max=1 default=0.5 label="Rounded"
20680
+ ${DEMO}`,
20681
+ SOUND,
20682
+ `float box(vec2 p, vec2 hs, float r) {
20683
+ vec2 q = abs(p) - hs + r;
20684
+ return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
20685
+ }
20686
+
20687
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
20688
+ vec2 uv = fragCoord / iResolution.xy;
20689
+ float px = 1.0 / iResolution.y;
20690
+ float n = float(uBars);
20691
+ // Margins round the bars, and a floor a little up from the bottom (the middle when mirrored).
20692
+ float x = (uv.x - 0.06) / 0.88;
20693
+ float base = uMirror ? 0.5 : 0.12;
20694
+ float room = uMirror ? 0.4 : 0.78;
20695
+ float cell = floor(x * n);
20696
+ float within = fract(x * n);
20697
+ vec3 col = uBackground * (1.0 + 0.25 * (1.0 - uv.y));
20698
+ if (x >= 0.0 && x < 1.0) {
20699
+ float v = clamp(spectrum((cell + 0.5) / n) * uGain, 0.0, 1.0);
20700
+ float h = max(v * room, 2.0 * px);
20701
+ // The bar, in pixels from its centre, so corners round alike at any size.
20702
+ float barW = (1.0 - uGap) * 0.88 * iResolution.x / n;
20703
+ vec2 p = vec2((within - 0.5) * 0.88 * iResolution.x / n, (uv.y - base) * iResolution.y);
20704
+ float sideY = uMirror ? abs(p.y) : p.y;
20705
+ float r = uRound * barW * 0.5;
20706
+ float d = box(vec2(p.x, sideY - h * 0.5 * iResolution.y), vec2(barW * 0.5, h * 0.5 * iResolution.y), r);
20707
+ float body = clamp(0.5 - d, 0.0, 1.0);
20708
+ float along = clamp(sideY / (room * iResolution.y), 0.0, 1.0);
20709
+ vec3 bar = mix(uColorLow, uColorHigh, along);
20710
+ // A soft glow under each bar, brighter as it rises.
20711
+ float halo = exp(-max(d, 0.0) / (barW * 0.6)) * 0.25 * v;
20712
+ col += bar * halo;
20713
+ col = mix(col, bar * (0.85 + 0.3 * v), body);
20714
+ if (uPeaks) {
20715
+ // The cap rides just above the bar, kicked up on each beat and falling back under gravity.
20716
+ float b = beat();
20717
+ float kick = b >= 0.0 ? max(0.0, 1.0 - b * 3.0) : 0.0;
20718
+ float lift = 0.035 + 0.12 * v * kick * kick;
20719
+ float capY = (h + lift * room) * iResolution.y;
20720
+ float cap = box(vec2(p.x, sideY - capY), vec2(barW * 0.5, max(2.0, 0.008 * iResolution.y)), r * 0.4);
20721
+ col = mix(col, mix(bar, vec3(1.0), 0.6), clamp(0.5 - cap, 0.0, 1.0));
20722
+ }
20723
+ // A faint reflection on the floor.
20724
+ if (!uMirror && p.y < 0.0) {
20725
+ float rd = box(vec2(p.x, -p.y - h * 0.5 * iResolution.y), vec2(barW * 0.5, h * 0.5 * iResolution.y), r);
20726
+ col = mix(col, bar, clamp(0.5 - rd, 0.0, 1.0) * 0.18 * exp(p.y / (0.06 * iResolution.y)));
20727
+ }
20728
+ }
20729
+ fragColor = vec4(col, 1.0);
20730
+ }`
20731
+ ),
20732
+ presets: [
20733
+ { name: "Mirrored neon", values: { uMirror: true, uBars: 48, uGap: 0.4, uColorLow: [0.6, 0.2, 1], uColorHigh: [0.2, 1, 0.8] } },
20734
+ { name: "Retro LED", values: { uBars: 16, uGap: 0.15, uRound: 0, uColorLow: [0.2, 1, 0.3], uColorHigh: [1, 0.2, 0.1], uBackground: [0, 0, 0] } },
20735
+ { name: "Minimal", values: { uBars: 64, uGap: 0.5, uRound: 1, uPeaks: false, uColorLow: [1, 1, 1], uColorHigh: [1, 1, 1], uBackground: [0.06, 0.06, 0.07] } }
20736
+ ]
20737
+ };
20738
+ var circularSpectrum = {
20739
+ kind: "shader",
20740
+ id: "circular-spectrum",
20741
+ name: "Circular spectrum",
20742
+ description: "A ring of spectrum bars radiating round a centre picture or colour, which swells on the bass.",
20743
+ category: "audio",
20744
+ video: "filter",
20745
+ tags: ["audio", "music", "spectrum", "radial", "ring", "visualizer", "album art"],
20746
+ source: glsl(
20747
+ `// Circular spectrum: bars radiating from a ring, the spectrum mirrored round it, a picture in the middle.
20748
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Centre picture"
20749
+ uniform bool uShowImage; // @control default=true label="Show the picture"
20750
+ uniform vec3 uCenter; // @control color default=#1E1B4B label="Centre colour"
20751
+ uniform float uRadius; // @control min=0.1 max=0.4 default=0.22 label="Radius"
20752
+ uniform int uBars; // @control min=24 max=180 default=96 label="Bars"
20753
+ uniform float uLength; // @control min=0.05 max=0.4 default=0.2 label="Bar length"
20754
+ uniform vec3 uColorA; // @control color default=#38BDF8 label="Colour 1"
20755
+ uniform vec3 uColorB; // @control color default=#E879F9 label="Colour 2"
20756
+ uniform vec3 uBackground; // @control color default=#05060F label="Background"
20757
+ uniform float uBackgroundAlpha; // @control min=0 max=1 default=1 label="Background opacity"
20758
+ uniform float uSpin; // @control min=-1 max=1 default=0.1 label="Spin"
20759
+ ${BACKDROP_CONTROLS}
20760
+ ${DEMO}`,
20761
+ PICTURE,
20762
+ SOUND,
20763
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
20764
+ float px = 1.0 / iResolution.y;
20765
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
20766
+ vec4 s = sound();
20767
+ float pulse = 1.0 + 0.12 * s.y * s.y;
20768
+ float r0 = uRadius * pulse;
20769
+ float r = length(p);
20770
+ float a = atan(p.x, p.y) + iTime * uSpin;
20771
+ // Bars round the ring: the spectrum low to high from the top down each side, so it is symmetric.
20772
+ float n = float(uBars);
20773
+ float t = fract(a / 6.28318);
20774
+ float cell = floor(t * n);
20775
+ float mirrored = abs((cell + 0.5) / n * 2.0 - 1.0);
20776
+ float v = spectrum(1.0 - mirrored);
20777
+ float len = uLength * (0.08 + v);
20778
+ float within = abs(fract(t * n) - 0.5) * 6.28318 * r / n;
20779
+ float width = 0.45 * 6.28318 * r0 / n;
20780
+ float radial = max(r0 + 0.01 - r, r - (r0 + 0.01 + len));
20781
+ float bar = clamp(0.5 - max(radial, within - width) / px, 0.0, 1.0);
20782
+ vec3 tint = mix(uColorA, uColorB, mirrored);
20783
+ vec4 acc = vec4(uBackground, uBackgroundAlpha);
20784
+ // Glow round the ring with the level.
20785
+ float halo = exp(-abs(r - r0 - 0.02) / (0.04 + 0.06 * s.x)) * (0.3 + 0.7 * s.x);
20786
+ vec3 glowCol = tint * halo * 0.6;
20787
+ acc.rgb += glowCol;
20788
+ acc.a = max(acc.a, clamp(halo * 0.6, 0.0, 1.0));
20789
+ acc = mix(acc, vec4(tint * (0.8 + 0.5 * v), 1.0), bar);
20790
+ // The centre: the picture (or the colour) in a disc, with a thin ring.
20791
+ float disc = clamp((r0 - r) / px, 0.0, 1.0);
20792
+ if (disc > 0.0) {
20793
+ vec2 uv = p / (2.0 * r0) + 0.5;
20794
+ vec4 pic = uShowImage ? pictureRGBA(uImage, vec2(uv.x, uv.y)) : vec4(uCenter, 1.0);
20795
+ vec3 c = mix(uCenter, pic.rgb, pic.a) * (0.9 + 0.2 * s.y);
20796
+ acc = mix(acc, vec4(c, 1.0), disc);
20797
+ }
20798
+ float rim = clamp(1.0 - abs(r - r0) / (1.5 * px), 0.0, 1.0);
20799
+ acc = mix(acc, vec4(mix(tint, vec3(1.0), 0.5), 1.0), rim * 0.8);
20800
+ fragColor = acc;
20801
+ }`
20802
+ ),
20803
+ presets: [
20804
+ { name: "Colour disc", values: { uShowImage: false, uCenter: [0.95, 0.3, 0.45], uColorA: [1, 0.8, 0.3], uColorB: [1, 0.3, 0.5] } },
20805
+ { name: "Overlay", values: { uBackgroundAlpha: 0, uShowImage: false, uCenter: [0, 0, 0], uBars: 140, uLength: 0.3 } },
20806
+ { name: "Dense", values: { uBars: 180, uLength: 0.32, uRadius: 0.18, uColorA: [0.3, 1, 0.7], uColorB: [0.2, 0.5, 1] } }
20807
+ ]
20808
+ };
20809
+ var waveformLine = {
20810
+ kind: "shader",
20811
+ id: "waveform-line",
20812
+ name: "Waveform line",
20813
+ description: "A smooth glowing oscilloscope line drawn from the music's spectrum, with fainter echoes trailing it.",
20814
+ category: "audio",
20815
+ tags: ["audio", "music", "waveform", "oscilloscope", "line", "visualizer", "overlay"],
20816
+ transparent: true,
20817
+ source: glsl(
20818
+ `// Waveform line: a line built from sines whose sizes are the spectrum's bands, glowing, with echoes.
20819
+ uniform vec3 uColor; // @control color default=#5EEAD4 label="Colour"
20820
+ uniform vec3 uEcho; // @control color default=#A78BFA label="Echo colour"
20821
+ uniform float uAmplitude; // @control min=0.05 max=0.5 default=0.22 label="Height"
20822
+ uniform float uThickness; // @control min=0.5 max=8 default=2.5 label="Thickness (px)"
20823
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow"
20824
+ uniform int uEchoes; // @control min=0 max=3 default=2 label="Echoes"
20825
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
20826
+ uniform vec3 uBackground; // @control color default=#03050C label="Background"
20827
+ uniform float uBackgroundAlpha; // @control min=0 max=1 default=0 label="Background opacity"
20828
+ ${DEMO}`,
20829
+ SOUND,
20830
+ GLOW8,
20831
+ `// The line's height at x (0\u20131) and its slope, from 12 bands of the spectrum, fading out at the ends.
20832
+ vec2 wave(float x, float t) {
20833
+ float y = 0.0;
20834
+ float dy = 0.0;
20835
+ for (int k = 0; k < 12; k++) {
20836
+ float fk = float(k);
20837
+ float amp = spectrum((fk + 0.5) / 12.0) / (1.0 + fk * 0.35);
20838
+ float freq = 6.28318 * (1.5 + fk * 1.7);
20839
+ float ph = t * (1.3 + fk * 0.6) * (mod(fk, 2.0) < 0.5 ? 1.0 : -1.0) + fk * 1.7;
20840
+ y += amp * sin(freq * x + ph);
20841
+ dy += amp * freq * cos(freq * x + ph);
20842
+ }
20843
+ float env = sin(3.14159 * clamp(x, 0.0, 1.0));
20844
+ float denv = 3.14159 * cos(3.14159 * clamp(x, 0.0, 1.0));
20845
+ return vec2(y * env, dy * env + y * denv) * 0.35;
20846
+ }
20847
+
20848
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
20849
+ vec2 uv = fragCoord / iResolution.xy;
20850
+ float aspect = iResolution.x / iResolution.y;
20851
+ vec4 s = sound();
20852
+ vec3 light = vec3(0.0);
20853
+ for (int e = 3; e >= 0; e--) {
20854
+ if (e > uEchoes) continue;
20855
+ float lag = float(e) * 0.12;
20856
+ vec2 w = wave(uv.x, iTime * uSpeed - lag) * uAmplitude * (1.0 + 0.6 * s.y) * (1.0 - 0.18 * float(e));
20857
+ // Distance to the line in pixels, corrected for its slope.
20858
+ float slope = w.y / aspect;
20859
+ float d = abs(uv.y - 0.5 - w.x) * iResolution.y / sqrt(1.0 + slope * slope);
20860
+ float core = clamp(uThickness * 0.5 + 0.5 - d, 0.0, 1.0);
20861
+ float halo = exp(-d / (6.0 + 18.0 * s.x)) * uGlow * 0.5;
20862
+ vec3 c = e == 0 ? uColor : uEcho;
20863
+ float fade = e == 0 ? 1.0 : 0.45 / float(e);
20864
+ light += c * (core * 1.6 + halo) * fade;
20865
+ }
20866
+ vec4 g = glow(light);
20867
+ vec3 col = mix(uBackground, g.rgb, g.a);
20868
+ float alpha = max(g.a, uBackgroundAlpha);
20869
+ fragColor = vec4(mix(g.rgb, col, uBackgroundAlpha), alpha);
20870
+ }`
20871
+ ),
20872
+ presets: [
20873
+ { name: "On black", values: { uBackgroundAlpha: 1 } },
20874
+ { name: "Laser", values: { uColor: [1, 0.15, 0.25], uEcho: [1, 0.6, 0.2], uThickness: 1.5, uGlow: 1.8 } },
20875
+ { name: "Clean", values: { uEchoes: 0, uGlow: 0.3, uColor: [1, 1, 1], uThickness: 3 } }
20876
+ ]
20877
+ };
20878
+ var bassPulse = {
20879
+ kind: "shader",
20880
+ id: "bass-pulse",
20881
+ name: "Bass pulse",
20882
+ description: "A picture or logo thumping to the bass: it swells and glows on every kick, with a colour split on the beat.",
20883
+ category: "audio",
20884
+ video: "filter",
20885
+ tags: ["audio", "music", "bass", "pulse", "logo", "beat", "glow"],
20886
+ source: glsl(
20887
+ `// Bass pulse: the picture scaled by the bass, a glow from its edges, a quick colour split on each beat.
20888
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Picture or logo"
20889
+ uniform float uSize; // @control min=0.2 max=1 default=0.55 label="Size"
20890
+ uniform float uPulse; // @control min=0 max=0.5 default=0.14 label="Pulse"
20891
+ uniform vec3 uGlowColor; // @control color default=#F472B6 label="Glow"
20892
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow strength"
20893
+ uniform float uSplit; // @control min=0 max=0.03 default=0.012 label="Colour split on the beat"
20894
+ uniform float uCorner; // @control min=0 max=0.5 default=0.08 label="Corner rounding"
20895
+ uniform vec3 uBackground; // @control color default=#0B0614 label="Background"
20896
+ uniform float uBackgroundAlpha; // @control min=0 max=1 default=1 label="Background opacity"
20897
+ ${BACKDROP_CONTROLS}
20898
+ ${DEMO}`,
20899
+ PICTURE,
20900
+ SOUND,
20901
+ `// The picture's coverage at p (centred, in heights), and its colour: inside a rounded box, through its own alpha.
20902
+ vec4 card(vec2 p, vec2 hs) {
20903
+ float corner = uCorner * min(hs.x, hs.y);
20904
+ vec2 q = abs(p) - hs + corner;
20905
+ float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - corner;
20906
+ float inside = clamp(0.5 - d * iResolution.y, 0.0, 1.0);
20907
+ if (inside <= 0.0) return vec4(0.0);
20908
+ vec4 c = pictureRGBA(uImage, p / (2.0 * hs) + 0.5);
20909
+ return vec4(c.rgb, c.a * inside);
20910
+ }
20911
+
20912
+ // The glow reaching p: falling off from the picture's box, shaped by its alpha blurred (a coarse mip) nearby.
20913
+ float glowAt(vec2 p, vec2 hs, float spread) {
20914
+ float corner = uCorner * min(hs.x, hs.y);
20915
+ vec2 q = abs(p) - hs + corner;
20916
+ float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - corner;
20917
+ float box = exp(-max(d, 0.0) / spread);
20918
+ if (textureSize(uImage, 0).x <= 1 || uBackdropAlpha >= 1.0) return box;
20919
+ // A logo on a clear backdrop: two coarse mips of its alpha, a soft halo round its own shape.
20920
+ vec2 uv = p / (2.0 * hs) + 0.5;
20921
+ float lod = log2(float(textureSize(uImage, 0).x));
20922
+ float blurred = min(1.0, 0.6 * textureLod(uImage, uv, lod - 4.5).a + 0.8 * textureLod(uImage, uv, lod - 3.0).a);
20923
+ float inside = step(abs(uv.x - 0.5), 0.5) * step(abs(uv.y - 0.5), 0.5);
20924
+ return mix(blurred * inside, box, uBackdropAlpha);
20925
+ }
20926
+
20927
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
20928
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
20929
+ vec4 s = sound();
20930
+ float b = beat();
20931
+ float hit = b >= 0.0 ? exp(-b * 6.0) : 0.0;
20932
+ float bass = s.y * s.y;
20933
+ float scale = 1.0 + uPulse * (bass * 0.7 + hit * 0.5);
20934
+ // The picture's own shape: square for a 1 \xD7 1 placeholder, else its aspect.
20935
+ vec2 size = vec2(textureSize(uImage, 0));
20936
+ float aspect = size.x > 1.0 ? size.x / size.y : 16.0 / 9.0;
20937
+ vec2 hs = 0.5 * uSize * scale * vec2(aspect, 1.0);
20938
+ // Narrower than the frame, whatever the picture's shape.
20939
+ hs *= min(1.0, 0.45 * iResolution.x / iResolution.y / hs.x);
20940
+ // A colour split along x on the beat.
20941
+ vec2 off = vec2(uSplit * hit, 0.0);
20942
+ vec4 c = card(p, hs);
20943
+ vec4 cr = card(p - off, hs);
20944
+ vec4 cb = card(p + off, hs);
20945
+ vec3 pic = vec3(cr.r, c.g, cb.b);
20946
+ float cover = max(c.a, max(cr.a, cb.a));
20947
+ // Glow round the picture, wider and brighter with the bass.
20948
+ float spread = 0.02 + 0.05 * bass + 0.03 * hit;
20949
+ float g = glowAt(p, hs, spread) * (0.45 + 0.9 * bass + 0.6 * hit) * uGlow;
20950
+ vec3 col = uBackground + uGlowColor * g * (1.0 - cover);
20951
+ float alpha = max(uBackgroundAlpha, clamp(g, 0.0, 1.0));
20952
+ col = mix(col, pic * (1.0 + 0.25 * hit), cover);
20953
+ fragColor = vec4(col, max(alpha, cover));
20954
+ }`
20955
+ ),
20956
+ presets: [
20957
+ { name: "Logo", values: { uImage: SAMPLE_IMAGES.logo, uBackdropAlpha: 0, uCorner: 0, uSize: 0.6, uGlowColor: [0.5, 0.5, 1] } },
20958
+ { name: "Heart beat", values: { uImage: SAMPLE_IMAGES.heart, uBackdropAlpha: 0, uCorner: 0, uPulse: 0.3, uGlowColor: [1, 0.3, 0.4] } },
20959
+ { name: "Overlay", values: { uBackgroundAlpha: 0, uSplit: 0.02 } }
20960
+ ]
20961
+ };
20962
+ var audioParticles = {
20963
+ kind: "shader",
20964
+ id: "audio-particles",
20965
+ name: "Audio particles",
20966
+ description: "Sparks bursting outward from the centre on every beat, trailing and fading, over a core that breathes with the level.",
20967
+ category: "audio",
20968
+ tags: ["audio", "music", "particles", "beat", "burst", "overlay", "visualizer"],
20969
+ transparent: true,
20970
+ source: glsl(
20971
+ `// Audio particles: a burst of sparks on each beat, flying out and slowing, plus a core glowing with the level.
20972
+ uniform int uCount; // @control min=8 max=64 default=40 label="Particles"
20973
+ uniform float uSpeed; // @control min=0.2 max=2 default=0.9 label="Burst speed"
20974
+ uniform float uSize; // @control min=0.002 max=0.02 default=0.007 label="Size"
20975
+ uniform float uLife; // @control min=0.2 max=2 default=0.9 label="Lifetime"
20976
+ uniform vec3 uColorA; // @control color default=#FDE047 label="Colour 1"
20977
+ uniform vec3 uColorB; // @control color default=#FB7185 label="Colour 2"
20978
+ uniform vec3 uColorC; // @control color default=#60A5FA label="Colour 3"
20979
+ uniform float uCore; // @control min=0 max=1 default=0.6 label="Core glow"
20980
+ ${DEMO}`,
20981
+ HASH,
20982
+ SOUND,
20983
+ GLOW8,
20984
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
20985
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
20986
+ vec4 s = sound();
20987
+ float b = beat();
20988
+ vec3 light = vec3(0.0);
20989
+ // The core breathes with the level.
20990
+ float r = length(p);
20991
+ light += mix(uColorA, uColorB, 0.5) * uCore * (0.2 + 0.8 * s.x) * exp(-r / (0.04 + 0.08 * s.y)) * 0.8;
20992
+ if (b >= 0.0 && b < uLife) {
20993
+ // Each burst is seeded by when it began (the beat's time on the iTime clock), so bursts differ.
20994
+ float seed = floor((iTime - b) * 8.0);
20995
+ float life = b / uLife;
20996
+ float fade = (1.0 - life) * (1.0 - life);
20997
+ float energy = 0.6 + 0.6 * s.y;
20998
+ for (int i = 0; i < 64; i++) {
20999
+ if (i >= uCount) break;
21000
+ vec2 h = hash22(vec2(float(i) * 1.91 + 0.3, seed));
21001
+ float ang = 6.28318 * (float(i) + h.x) / float(uCount);
21002
+ float v = uSpeed * energy * (0.35 + 0.65 * h.y);
21003
+ // Out fast, slowed by drag, the slow ones drooping a little.
21004
+ float dist = v * (1.0 - exp(-b * 3.5)) / 3.5;
21005
+ vec2 at = vec2(sin(ang), cos(ang)) * dist - vec2(0.0, 0.08 * b * b);
21006
+ vec2 dir = normalize(vec2(sin(ang), cos(ang)) + 1e-4);
21007
+ // A short streak behind each spark, along its direction.
21008
+ vec2 d = p - at;
21009
+ float along = clamp(dot(d, -dir), 0.0, 0.04 * exp(-b * 3.0) + 0.002);
21010
+ float dd = length(d + dir * along);
21011
+ float size = uSize * (1.0 - 0.5 * life);
21012
+ float spark = exp(-dd * dd / (size * size)) + 0.25 * exp(-dd / (size * 4.0));
21013
+ vec3 c = h.x < 0.33 ? uColorA : h.x < 0.66 ? uColorB : uColorC;
21014
+ light += c * spark * fade * 1.4;
21015
+ }
21016
+ }
21017
+ // Fine glitter that shimmers with the treble.
21018
+ vec2 cell = floor(p * 40.0);
21019
+ float g = hash12(cell + floor(iTime * 12.0));
21020
+ float twinkle = step(0.985, g) * s.w * exp(-length(fract(p * 40.0) - 0.5) * 8.0);
21021
+ light += uColorC * twinkle;
21022
+ fragColor = glow(light);
21023
+ }`
21024
+ ),
21025
+ presets: [
21026
+ { name: "Gold", values: { uColorA: [1, 0.85, 0.4], uColorB: [1, 0.6, 0.2], uColorC: [1, 1, 0.8], uCount: 56 } },
21027
+ { name: "Ice", values: { uColorA: [0.6, 0.9, 1], uColorB: [0.3, 0.6, 1], uColorC: [1, 1, 1], uSpeed: 1.3, uLife: 1.2 } }
21028
+ ]
21029
+ };
21030
+ var audioPlasma = {
21031
+ kind: "shader",
21032
+ id: "audio-plasma",
21033
+ name: "Audio plasma",
21034
+ description: "A flowing plasma whose swirl, scale and colours follow the bass, mids and highs, flaring on each beat.",
21035
+ category: "audio",
21036
+ tags: ["audio", "music", "plasma", "gradient", "psychedelic", "background", "visualizer"],
21037
+ source: glsl(
21038
+ `// Audio plasma: layered sines warped by noise; the bass pushes the flow, the mids tilt the palette, the highs add grain.
21039
+ uniform float uScale; // @control min=0.5 max=6 default=2.4 label="Scale"
21040
+ uniform float uFlow; // @control min=0 max=2 default=0.5 label="Flow"
21041
+ uniform float uReact; // @control min=0 max=2 default=1 label="Reactivity"
21042
+ uniform vec3 uPhase; // @control color default=#1A66B3 label="Palette phase"
21043
+ uniform float uContrast; // @control min=0.5 max=2 default=1.1 label="Contrast"
21044
+ ${DEMO}`,
21045
+ HASH,
21046
+ NOISE,
21047
+ PALETTE,
21048
+ SOUND,
21049
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21050
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
21051
+ vec4 s = sound() * uReact;
21052
+ float b = beat();
21053
+ float hit = b >= 0.0 ? exp(-b * 5.0) * uReact : 0.0;
21054
+ float t = iTime * uFlow;
21055
+ // The bass pushes the pattern on (a nudge that settles back, not a jump in time).
21056
+ float push = 0.8 * s.y + 0.5 * hit;
21057
+ vec2 q = p * uScale * (1.0 - 0.1 * s.y);
21058
+ vec2 w = vec2(vnoise(q * 0.7 + t * 0.3), vnoise(q * 0.7 - t * 0.27 + 5.0)) - 0.5;
21059
+ q += w * (1.2 + 1.5 * s.z);
21060
+ float v = sin(q.x + t + push);
21061
+ v += sin(q.y * 1.3 - t * 0.8 + push * 0.7);
21062
+ v += sin((q.x + q.y) * 0.8 + t * 0.6);
21063
+ v += sin(length(q + vec2(sin(t * 0.4), cos(t * 0.3))) * 2.0 - t * 1.2 - push * 1.5);
21064
+ v = v * 0.25 * uContrast;
21065
+ // The mids turn the palette; the highs add a fine shimmer.
21066
+ vec3 col = palette(v * 0.7 + 0.15 * s.z + 0.05 * t, vec3(0.45, 0.4, 0.5), vec3(0.4, 0.35, 0.4), vec3(0.8, 0.7, 0.6), uPhase + vec3(0.0, 0.1 * s.w, 0.2 * s.y));
21067
+ col *= 0.75 + 0.35 * s.x + 0.4 * hit;
21068
+ col += (hash12(fragCoord + floor(iTime * 30.0)) - 0.5) * 0.06 * s.w;
21069
+ fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
21070
+ }`
21071
+ ),
21072
+ presets: [
21073
+ { name: "Sunset", values: { uPhase: [0, 0.15, 0.3], uScale: 1.6 } },
21074
+ { name: "Acid", values: { uPhase: [0.3, 0.6, 0.1], uScale: 4, uContrast: 1.6, uFlow: 1 } },
21075
+ { name: "Calm", values: { uReact: 0.4, uFlow: 0.25, uScale: 1.2 } }
21076
+ ]
21077
+ };
21078
+ var vinyl = {
21079
+ kind: "shader",
21080
+ id: "vinyl",
21081
+ name: "Vinyl",
21082
+ description: "A record spinning on a turntable, its label a picture, its grooves catching the light with the music, the tonearm riding the beat.",
21083
+ category: "audio",
21084
+ video: "stills",
21085
+ tags: ["audio", "music", "vinyl", "record", "turntable", "retro", "album art"],
21086
+ source: glsl(
21087
+ `// Vinyl: a spinning record; each groove's sheen follows the spectrum at its radius, the label is a picture.
21088
+ uniform sampler2D uLabel; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Label picture"
21089
+ uniform float uRpm; // @control min=0 max=78 default=33.3 label="Speed (rpm)"
21090
+ uniform vec3 uRecord; // @control color default=#101014 label="Record"
21091
+ uniform vec3 uSheen; // @control color default=#C4B5FD label="Sheen"
21092
+ uniform float uShimmer; // @control min=0 max=2 default=1 label="Shimmer"
21093
+ uniform vec3 uBackground; // @control color default=#2A211C label="Platter"
21094
+ uniform float uBackgroundAlpha; // @control min=0 max=1 default=1 label="Platter opacity"
21095
+ uniform bool uArm; // @control default=true label="Tonearm"
21096
+ ${BACKDROP_CONTROLS}
21097
+ ${DEMO}`,
21098
+ PICTURE,
21099
+ SOUND,
21100
+ ROTATE,
21101
+ `float segment(vec2 p, vec2 a, vec2 b) {
21102
+ vec2 ab = b - a;
21103
+ float t = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0);
21104
+ return length(p - a - ab * t);
21105
+ }
21106
+
21107
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21108
+ float px = 1.0 / iResolution.y;
21109
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
21110
+ vec2 c = p + vec2(0.12, 0.0);
21111
+ vec4 s = sound();
21112
+ float b = beat();
21113
+ float hit = b >= 0.0 ? exp(-b * 8.0) : 0.0;
21114
+ float R = 0.44;
21115
+ float r = length(c);
21116
+ float spin = iTime * uRpm / 60.0 * 6.28318;
21117
+ vec2 q = rot(spin) * c;
21118
+ float ang = atan(q.y, q.x);
21119
+ // The platter, with a soft shadow under the record.
21120
+ vec3 col = uBackground * (0.85 + 0.15 * smoothstep(0.9, 0.0, length(p)));
21121
+ float shadow = 0.5 * exp(-max(r - R, 0.0) / 0.02) * step(R, r);
21122
+ col *= 1.0 - shadow;
21123
+ float alpha = max(uBackgroundAlpha, shadow);
21124
+ float disc = clamp((R - r) / px, 0.0, 1.0);
21125
+ if (disc > 0.0) {
21126
+ // Grooves: fine rings; their sheen is the spectrum at that radius (bass outside, treble in), in two
21127
+ // bright wedges opposite the light, as on a real record.
21128
+ float band = clamp((R - r) / (R - 0.14), 0.0, 1.0);
21129
+ float level = spectrum(band);
21130
+ float grooves = 0.5 + 0.5 * sin(r * 900.0);
21131
+ float wedge = pow(abs(cos(atan(c.y, c.x) - 0.8)), 24.0);
21132
+ float sheen = wedge * (0.25 + uShimmer * level) * (0.6 + 0.4 * grooves);
21133
+ vec3 rec = uRecord * (0.9 + 0.2 * grooves) + uSheen * sheen;
21134
+ // A faint swirl that shows the spin.
21135
+ rec += 0.03 * sin(ang * 2.0 + r * 40.0);
21136
+ // Lead-in and run-out rings.
21137
+ rec *= 1.0 - 0.4 * (smoothstep(0.004, 0.0, abs(r - R + 0.012)) + smoothstep(0.004, 0.0, abs(r - 0.15)));
21138
+ col = mix(col, rec, disc);
21139
+ // The label: the picture, turning with the record.
21140
+ float label = clamp((0.13 - r) / px, 0.0, 1.0);
21141
+ vec4 pic = pictureRGBA(uLabel, q / 0.26 + 0.5);
21142
+ vec3 lab = mix(uRecord, pic.rgb, pic.a) * (0.95 + 0.1 * s.y);
21143
+ col = mix(col, lab, label);
21144
+ // The spindle.
21145
+ col = mix(col, vec3(0.8), clamp((0.008 - r) / px, 0.0, 1.0));
21146
+ }
21147
+ if (uArm) {
21148
+ // The tonearm: from a pivot at the top right to the stylus on the outer grooves, nudged by the beat.
21149
+ vec2 pivot = vec2(0.42, 0.34);
21150
+ float swing = 0.32 + 0.004 * hit;
21151
+ vec2 head = pivot + vec2(-sin(swing + 0.9), -cos(swing + 0.9)) * 0.5;
21152
+ float arm = segment(p, pivot, head);
21153
+ float shade = clamp((0.009 - arm) / px, 0.0, 1.0);
21154
+ col = mix(col * (1.0 - 0.35 * exp(-segment(p + vec2(-0.01, 0.015), pivot, head) / 0.015)), vec3(0.78, 0.78, 0.82), shade);
21155
+ float cart = segment(p, head, head + vec2(-0.03, -0.012));
21156
+ col = mix(col, vec3(0.15), clamp((0.016 - cart) / px, 0.0, 1.0));
21157
+ float base = length(p - pivot);
21158
+ float foot = clamp((0.035 - base) / px, 0.0, 1.0);
21159
+ col = mix(col, vec3(0.6), foot);
21160
+ col = mix(col, vec3(0.3), clamp((0.015 - base) / px, 0.0, 1.0));
21161
+ alpha = max(alpha, max(shade, max(foot, clamp((0.016 - cart) / px, 0.0, 1.0))));
21162
+ }
21163
+ fragColor = vec4(col, max(alpha, disc));
21164
+ }`
21165
+ ),
21166
+ presets: [
21167
+ { name: "Red vinyl", values: { uRecord: [0.45, 0.04, 0.06], uSheen: [1, 0.6, 0.6] } },
21168
+ { name: "45 rpm", values: { uRpm: 45, uLabel: SAMPLE_IMAGES.logo, uBackdrop: [0.95, 0.9, 0.8] } }
21169
+ ]
21170
+ };
21171
+ var audioShaders = [spectrumBars, circularSpectrum, waveformLine, bassPulse, audioParticles, audioPlasma, vinyl];
21172
+
21173
+ // src/catalog/shaders/scroll.ts
21174
+ var SCROLL = `
21175
+ bool scrollDemo() { return uDemo && dot(iScroll, iScroll) == 0.0; }
21176
+ vec4 scroll() {
21177
+ if (!scrollDemo()) return iScroll;
21178
+ // Down and back up every 6 s, resting a moment at each end.
21179
+ float ph = fract(iTime / 6.0);
21180
+ float u = clamp(ph < 0.5 ? ph * 2.4 - 0.1 : (1.0 - ph) * 2.4 - 0.1, 0.0, 1.0);
21181
+ float x = u * u * (3.0 - 2.0 * u);
21182
+ float moving = u > 0.0 && u < 1.0 ? 1.0 : 0.0;
21183
+ // dx/dt in viewport heights a second (progress 0 to 1 being about two viewport heights): u moves 2.4 per 6 s.
21184
+ float v = moving * 6.0 * u * (1.0 - u) * (2.4 / 6.0) * 2.0 * (ph < 0.5 ? 1.0 : -1.0);
21185
+ return vec4(x, x, v, moving);
21186
+ }`.trim();
21187
+ var DEMO2 = `uniform bool uDemo; // @control default=true label="Demo when not scrolled"`;
21188
+ var scrollReveal = {
21189
+ kind: "shader",
21190
+ id: "scroll-reveal",
21191
+ name: "Scroll reveal",
21192
+ description: "A picture revealed by a soft wipe, dissolve, iris or blinds as it scrolls into view, settling from a slight zoom.",
21193
+ category: "scroll",
21194
+ video: "filter",
21195
+ transparent: true,
21196
+ tags: ["interactive", "scroll", "reveal", "wipe", "dissolve", "image", "landing page"],
21197
+ source: glsl(
21198
+ `// Scroll reveal: the picture's coverage follows how far the canvas has come into view.
21199
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
21200
+ uniform int uStyle; // @control min=0 max=3 default=0 label="Style (wipe, dissolve, iris, blinds)"
21201
+ uniform float uStart; // @control min=0 max=1 default=0.1 label="Starts at"
21202
+ uniform float uEnd; // @control min=0 max=1 default=0.5 label="Done at"
21203
+ uniform float uSoftness; // @control min=0 max=0.4 default=0.12 label="Edge softness"
21204
+ uniform float uZoom; // @control min=0 max=0.3 default=0.08 label="Settle zoom"
21205
+ uniform vec3 uEdge; // @control color default=#FFFFFF label="Edge glow"
21206
+ uniform float uEdgeGlow; // @control min=0 max=1 default=0.35 label="Edge glow strength"
21207
+ ${BACKDROP_CONTROLS}
21208
+ ${DEMO2}`,
21209
+ PICTURE,
21210
+ HASH,
21211
+ NOISE,
21212
+ SCROLL,
21213
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21214
+ vec2 uv = fragCoord / iResolution.xy;
21215
+ vec4 s = scroll();
21216
+ float r = smoothstep(uStart, max(uEnd, uStart + 1e-3), s.x);
21217
+ // The field the reveal sweeps across, 0\u20131, and how far it has swept.
21218
+ float field;
21219
+ if (uStyle == 0) field = 1.0 - uv.y + 0.04 * (vnoise(vec2(uv.x * 9.0, 0.0)) - 0.5);
21220
+ else if (uStyle == 1) field = vnoise(fragCoord / iResolution.y * 18.0) * 0.6 + vnoise(fragCoord / iResolution.y * 60.0) * 0.4;
21221
+ else if (uStyle == 2) field = length((uv - 0.5) * vec2(iResolution.x / iResolution.y, 1.0)) / 0.9;
21222
+ else field = fract(uv.y * 8.0) * 0.9 + uv.y * 0.1;
21223
+ float soft = max(uSoftness, 1e-3);
21224
+ float edge = r * (1.0 + soft) - soft;
21225
+ float shown = smoothstep(edge + soft, edge, field);
21226
+ float rim = exp(-pow((field - edge - soft * 0.5) / (soft * 0.35 + 0.01), 2.0)) * (1.0 - step(0.999, r)) * step(0.001, r);
21227
+ // The picture settles from a slight zoom as it is revealed.
21228
+ vec2 at = (uv - 0.5) / (1.0 + uZoom * (1.0 - r)) + 0.5;
21229
+ vec4 pic = pictureRGBA(uImage, at);
21230
+ vec3 col = pic.rgb + uEdge * rim * uEdgeGlow;
21231
+ float alpha = pic.a * shown;
21232
+ fragColor = vec4(col, clamp(max(alpha, rim * uEdgeGlow * pic.a), 0.0, 1.0));
21233
+ }`
21234
+ ),
21235
+ presets: [
21236
+ { name: "Dissolve", values: { uStyle: 1, uSoftness: 0.2 } },
21237
+ { name: "Iris", values: { uStyle: 2, uSoftness: 0.05, uImage: SAMPLE_IMAGES.portrait } },
21238
+ { name: "Blinds", values: { uStyle: 3, uSoftness: 0.05, uEdgeGlow: 0, uImage: SAMPLE_IMAGES.city } }
21239
+ ]
21240
+ };
21241
+ var parallaxLayers = {
21242
+ kind: "shader",
21243
+ id: "parallax-layers",
21244
+ name: "Parallax layers",
21245
+ description: "Layered hills under a sky, the near ones sliding further than the far ones as the page scrolls, for depth.",
21246
+ category: "scroll",
21247
+ tags: ["interactive", "scroll", "parallax", "landscape", "hills", "depth", "hero", "background"],
21248
+ source: glsl(
21249
+ `// Parallax layers: five ridgelines, each shifted by the scroll in proportion to how near it is.
21250
+ uniform vec3 uSkyTop; // @control color default=#1E3A8A label="Sky top"
21251
+ uniform vec3 uSkyBottom; // @control color default=#FDBA74 label="Sky horizon"
21252
+ uniform vec3 uFar; // @control color default=#A78BFA label="Far hills"
21253
+ uniform vec3 uNear; // @control color default=#1E1B4B label="Near hills"
21254
+ uniform vec3 uSun; // @control color default=#FFF7D6 label="Sun"
21255
+ uniform float uDepth; // @control min=0 max=1.5 default=0.6 label="Parallax depth"
21256
+ uniform float uRough; // @control min=0 max=1 default=0.5 label="Ruggedness"
21257
+ uniform float uDrift; // @control min=0 max=0.2 default=0.02 label="Drift"
21258
+ ${DEMO2}`,
21259
+ HASH,
21260
+ NOISE,
21261
+ SCROLL,
21262
+ `float ridge(float x, float seed) {
21263
+ float h = vnoise(vec2(x * 1.3, seed)) * 0.6 + vnoise(vec2(x * 3.1, seed + 7.0)) * 0.3 * (0.5 + uRough) + vnoise(vec2(x * 8.0, seed + 3.0)) * 0.1 * uRough;
21264
+ return h;
21265
+ }
21266
+
21267
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21268
+ vec2 uv = fragCoord / iResolution.xy;
21269
+ float aspect = iResolution.x / iResolution.y;
21270
+ vec4 s = scroll();
21271
+ float shift = (s.x - 0.5) * uDepth;
21272
+ // The sky (and sun) move least: further away.
21273
+ float skyY = uv.y + shift * 0.15;
21274
+ vec3 col = mix(uSkyBottom, uSkyTop, smoothstep(0.1, 1.0, skyY));
21275
+ vec2 sun = vec2(0.68 * aspect, 0.62 - shift * 0.2);
21276
+ float d = length(vec2(uv.x * aspect, uv.y) - sun);
21277
+ col = mix(col, uSun, smoothstep(0.075, 0.07, d));
21278
+ col += uSun * exp(-d * 6.0) * 0.25;
21279
+ for (int i = 0; i < 5; i++) {
21280
+ float k = float(i) / 4.0;
21281
+ // Nearer layers sit lower, rise higher with the scroll, and drift faster.
21282
+ float base = 0.55 - 0.1 * float(i) + shift * (0.15 + 0.85 * k * k) ;
21283
+ float x = uv.x * aspect * (0.6 + 0.5 * k) + iTime * uDrift * (0.3 + k) + float(i) * 11.3;
21284
+ float top = base + ridge(x, float(i) * 5.0) * (0.18 + 0.05 * k);
21285
+ float layer = smoothstep(top + 1.5 / iResolution.y, top, uv.y);
21286
+ vec3 tint = mix(mix(uFar, uSkyBottom, 0.35), uNear, k);
21287
+ // Mist at each layer's foot.
21288
+ tint = mix(tint, uSkyBottom, 0.25 * (1.0 - k) * smoothstep(top - 0.25, top, uv.y) * 0.5);
21289
+ col = mix(col, tint, layer);
21290
+ }
21291
+ fragColor = vec4(col, 1.0);
21292
+ }`
21293
+ ),
21294
+ presets: [
21295
+ { name: "Night", values: { uSkyTop: [0.02, 0.03, 0.1], uSkyBottom: [0.15, 0.2, 0.4], uFar: [0.2, 0.25, 0.45], uNear: [0.02, 0.02, 0.06], uSun: [0.9, 0.92, 1] } },
21296
+ { name: "Forest", values: { uSkyTop: [0.55, 0.78, 0.95], uSkyBottom: [0.9, 0.95, 0.9], uFar: [0.45, 0.65, 0.55], uNear: [0.05, 0.2, 0.12], uRough: 0.9 } },
21297
+ { name: "Desert", values: { uSkyTop: [0.95, 0.55, 0.3], uSkyBottom: [1, 0.85, 0.6], uFar: [0.85, 0.55, 0.4], uNear: [0.45, 0.2, 0.12], uRough: 0.2 } }
21298
+ ]
21299
+ };
21300
+ var scrollProgressGlow = {
21301
+ kind: "shader",
21302
+ id: "scroll-progress-glow",
21303
+ name: "Scroll progress glow",
21304
+ description: "A reading-progress bar along the top of the page, filling as you scroll, its glowing head flaring while you move.",
21305
+ category: "scroll",
21306
+ transparent: true,
21307
+ tags: ["interactive", "scroll", "progress", "reading", "bar", "overlay", "ui"],
21308
+ source: glsl(
21309
+ `// Scroll progress glow: a bar as long as the page's scroll, with a glow under it and a bright head.
21310
+ uniform vec3 uColorA; // @control color default=#22D3EE label="Start colour"
21311
+ uniform vec3 uColorB; // @control color default=#A855F7 label="End colour"
21312
+ uniform float uHeight; // @control min=1 max=16 default=4 label="Thickness (px)"
21313
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow"
21314
+ uniform bool uBottom; // @control default=false label="Along the bottom"
21315
+ uniform vec3 uTrack; // @control color default=#FFFFFF label="Track"
21316
+ uniform float uTrackAlpha; // @control min=0 max=1 default=0.08 label="Track opacity"
21317
+ ${DEMO2}`,
21318
+ SCROLL,
21319
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21320
+ vec4 s = scroll();
21321
+ float progress = clamp(s.y, 0.0, 1.0);
21322
+ float x = fragCoord.x;
21323
+ // Pixels from the bar's edge, into the page.
21324
+ float y = uBottom ? fragCoord.y : iResolution.y - fragCoord.y;
21325
+ float end = progress * iResolution.x;
21326
+ float inBar = clamp(uHeight - y + 0.5, 0.0, 1.0);
21327
+ float filled = clamp(end - x + 0.5, 0.0, 1.0);
21328
+ vec3 tint = mix(uColorA, uColorB, x / iResolution.x);
21329
+ float lit = 0.35 + 0.65 * s.w;
21330
+ // A glow spilling down from the filled part, and a flare round the head while scrolling.
21331
+ float under = filled * exp(-max(y - uHeight, 0.0) / (6.0 + 10.0 * s.w)) * 0.5 * uGlow * lit;
21332
+ float head = exp(-length(vec2(x - end, max(y - uHeight * 0.5, 0.0))) / (8.0 + 20.0 * s.w)) * uGlow * (0.4 + 0.9 * s.w) * step(0.001, progress);
21333
+ vec3 light = tint * (under + head * 1.4);
21334
+ vec4 bar = vec4(mix(uTrack, tint * (1.0 + 0.3 * s.w), filled), inBar * mix(uTrackAlpha, 1.0, filled));
21335
+ // Light over the bar, as straight colour with alpha.
21336
+ vec3 g = 1.0 - exp(-light);
21337
+ float ga = max(g.r, max(g.g, g.b));
21338
+ float alpha = bar.a + ga * (1.0 - bar.a);
21339
+ vec3 col = (bar.rgb * bar.a + g * (1.0 - bar.a)) / max(alpha, 1e-4);
21340
+ fragColor = vec4(col, alpha);
21341
+ }`
21342
+ ),
21343
+ presets: [
21344
+ { name: "Fire", values: { uColorA: [1, 0.8, 0.2], uColorB: [1, 0.2, 0.1], uHeight: 3 } },
21345
+ { name: "Thick, bottom", values: { uBottom: true, uHeight: 10, uGlow: 0.5, uTrackAlpha: 0.15 } }
21346
+ ]
21347
+ };
21348
+ var scrollVelocityBlur = {
21349
+ kind: "shader",
21350
+ id: "scroll-velocity-blur",
21351
+ name: "Scroll velocity blur",
21352
+ description: "A picture smeared with motion blur and a colour split as fast as the page scrolls, sharp again when it stops.",
21353
+ category: "scroll",
21354
+ video: "filter",
21355
+ tags: ["interactive", "scroll", "motion blur", "speed", "image", "chromatic"],
21356
+ source: glsl(
21357
+ `// Scroll velocity blur: samples along the scroll direction, as many viewport heights as it is moving per second.
21358
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.city} label="Image"
21359
+ uniform float uAmount; // @control min=0 max=0.3 default=0.08 label="Blur per speed"
21360
+ uniform float uMax; // @control min=0.01 max=0.4 default=0.15 label="Most blur"
21361
+ uniform float uSplit; // @control min=0 max=1 default=0.4 label="Colour split"
21362
+ uniform float uStretch; // @control min=0 max=0.2 default=0.04 label="Stretch"
21363
+ ${BACKDROP_CONTROLS}
21364
+ ${DEMO2}`,
21365
+ PICTURE,
21366
+ SCROLL,
21367
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21368
+ vec2 uv = fragCoord / iResolution.xy;
21369
+ vec4 s = scroll();
21370
+ float v = s.z;
21371
+ float len = clamp(abs(v) * uAmount, 0.0, uMax) * sign(v);
21372
+ // A slight stretch along the scroll while moving.
21373
+ uv.y = (uv.y - 0.5) / (1.0 + uStretch * abs(len) / max(uMax, 1e-3)) + 0.5;
21374
+ vec4 acc = vec4(0.0);
21375
+ vec3 split = vec3(0.0);
21376
+ for (int i = 0; i < 16; i++) {
21377
+ float t = (float(i) + 0.5) / 16.0 - 0.5;
21378
+ float w = 1.0 - abs(t) * 1.2;
21379
+ vec4 c = pictureRGBA(uImage, uv + vec2(0.0, t * len));
21380
+ acc += c * w;
21381
+ // Red trails, blue leads: a colour split along the motion.
21382
+ split += vec3(c.r * max(-t, 0.0), 0.0, c.b * max(t, 0.0)) * w;
21383
+ }
21384
+ acc /= 6.4;
21385
+ split /= 6.4;
21386
+ vec3 col = acc.rgb + (split * 2.0 - vec3(acc.r, 0.0, acc.b) * 0.5) * uSplit * clamp(abs(len) * 10.0, 0.0, 1.0);
21387
+ fragColor = vec4(clamp(col, 0.0, 1.0), clamp(acc.a, 0.0, 1.0));
21388
+ }`
21389
+ ),
21390
+ presets: [
21391
+ { name: "Subtle", values: { uAmount: 0.03, uSplit: 0.1, uStretch: 0 } },
21392
+ { name: "Warp speed", values: { uAmount: 0.2, uMax: 0.35, uSplit: 1, uStretch: 0.15 } }
21393
+ ]
21394
+ };
21395
+ var scrollZoom = {
21396
+ kind: "shader",
21397
+ id: "scroll-zoom",
21398
+ name: "Scroll zoom",
21399
+ description: "A picture zooming in (or out) and drifting as it passes through the viewport, with an optional turn and vignette.",
21400
+ category: "scroll",
21401
+ video: "filter",
21402
+ tags: ["interactive", "scroll", "zoom", "ken burns", "image", "hero"],
21403
+ source: glsl(
21404
+ `// Scroll zoom: the picture's scale, pan and turn follow its progress through the viewport.
21405
+ uniform sampler2D uImage; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
21406
+ uniform float uFrom; // @control min=0.5 max=3 default=1 label="Zoom entering"
21407
+ uniform float uTo; // @control min=0.5 max=3 default=1.6 label="Zoom leaving"
21408
+ uniform vec2 uFocus; // @control min=0 max=1 default=0.62,0.5 label="Zoom towards"
21409
+ uniform float uPan; // @control min=-0.3 max=0.3 default=0.06 label="Vertical drift"
21410
+ uniform float uTurn; // @control min=-30 max=30 default=0 label="Turn (degrees)"
21411
+ uniform float uVignette; // @control min=0 max=1 default=0.3 label="Vignette"
21412
+ ${BACKDROP_CONTROLS}
21413
+ ${DEMO2}`,
21414
+ PICTURE,
21415
+ SCROLL,
21416
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
21417
+ vec2 uv = fragCoord / iResolution.xy;
21418
+ vec4 s = scroll();
21419
+ float k = s.x * s.x * (3.0 - 2.0 * s.x);
21420
+ float zoom = mix(uFrom, uTo, k);
21421
+ float a = radians(uTurn) * (k - 0.5);
21422
+ vec2 aspect = vec2(iResolution.x / iResolution.y, 1.0);
21423
+ vec2 p = (uv - uFocus) * aspect;
21424
+ p = mat2(cos(a), sin(a), -sin(a), cos(a)) * p / zoom;
21425
+ vec2 at = p / aspect + uFocus + vec2(0.0, uPan * (k - 0.5));
21426
+ vec4 pic = pictureRGBA(uImage, at);
21427
+ float vig = 1.0 - uVignette * smoothstep(0.35, 0.95, length((uv - 0.5) * aspect) / length(aspect * 0.5));
21428
+ fragColor = vec4(pic.rgb * vig, pic.a);
21429
+ }`
21430
+ ),
21431
+ presets: [
21432
+ { name: "Zoom out", values: { uFrom: 1.8, uTo: 1 } },
21433
+ { name: "Dramatic turn", values: { uTo: 2.2, uTurn: 18, uVignette: 0.6, uImage: SAMPLE_IMAGES.city } }
21434
+ ]
21435
+ };
21436
+ var scrollShaders = [scrollReveal, parallaxLayers, scrollProgressGlow, scrollVelocityBlur, scrollZoom];
21437
+
21438
+ // src/catalog/scroll.ts
21439
+ var n10 = (name) => `{{${name}}}`;
21440
+ var SANS7 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
21441
+ var scrollDraw = {
21442
+ kind: "vector",
21443
+ id: "scroll-draw",
21444
+ name: "Scroll draw",
21445
+ description: "A line drawing itself along its path as the page scrolls, a dot riding its tip, and undrawing as you scroll back.",
21446
+ category: "scroll",
21447
+ trigger: "scroll",
21448
+ tags: ["interactive", "scroll", "line", "path", "draw", "route", "storytelling"],
21449
+ motion: {
21450
+ svg: `<svg viewBox="0 0 400 200" fill="none"><!-- {{accent: color default=#6366F1 label='Line'}} {{track: color default=#CBD5E1 label='Track'}} {{width: float min=1 max=16 default=6 step=1 label='Width'}} --><path d="{{path: text default='M24 160 C84 20 150 24 196 100 S318 186 376 44' label='Path'}}" stroke="{{track}}" stroke-width="2" stroke-dasharray="2 8" stroke-linecap="round"/><path id="scrollDrawLine" d="{{path}}" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round" pathLength="1" stroke-dasharray="1 1" stroke-dashoffset="1"/><circle id="scrollDrawDot" cx="0" cy="0" r="{{dot}}" fill="{{accent}}" stroke="#FFFFFF" stroke-width="3"/></svg>`,
21451
+ animate: [
21452
+ { id: "scrollDrawLine", draw: true, ms: 1e3, ease: "linear" },
21453
+ { id: "scrollDrawDot", along: "scrollDrawLine", ms: 1e3, ease: "linear" }
21454
+ ]
21455
+ },
21456
+ derived: (v) => ({ dot: Math.round((Number(v.width) * 0.9 + 4) * 10) / 10 }),
21457
+ presets: [
21458
+ { name: "Signature", values: { path: "M30 140 C60 60 90 60 100 120 C110 170 140 70 170 90 C200 110 190 150 220 130 C250 110 270 80 300 110 C320 130 350 120 370 90", width: 4, accent: [0.07, 0.09, 0.15] } },
21459
+ { name: "Bold", values: { width: 12, accent: [0.93, 0.27, 0.27] } }
21460
+ ]
21461
+ };
21462
+ var scrollCounter = {
21463
+ kind: "vector",
21464
+ id: "scroll-counter",
21465
+ name: "Scroll counter",
21466
+ description: "A big number counting up as it scrolls into view, with its label and a line growing under it.",
21467
+ category: "scroll",
21468
+ trigger: "scroll",
21469
+ tags: ["interactive", "scroll", "counter", "number", "stat", "kpi", "landing page"],
21470
+ motion: {
21471
+ svg: `<svg viewBox="0 0 400 200" font-family="${SANS7}"><!-- {{accent: color default=#0EA5E9 label='Accent'}} {{ink: color default=#0F172A label='Number'}} {{muted: color default=#64748B label='Label colour'}} {{value: float min=0 max=1000000 default=2400 step=1 label='Number'}} {{decimals: int min=0 max=3 default=0 label='Decimals'}} --><text id="scrollCounterValue" x="200" y="112" text-anchor="middle" font-size="76" font-weight="800" fill="{{ink}}" style="font-variant-numeric: tabular-nums">0</text><rect id="scrollCounterLine" x="140" y="132" width="0" height="6" rx="3" fill="{{accent}}"/><text x="200" y="170" text-anchor="middle" font-size="20" font-weight="500" fill="{{muted}}">{{label: text default='happy customers' label='Label'}}</text></svg>`,
21472
+ animate: [
21473
+ { id: "scrollCounterValue", count: [0, n10("value")], format: "{{prefix: text default='' label='Before the number'}}{n}{{suffix: text default='+' label='After the number'}}", decimals: n10("decimals"), ms: 1e3, ease: "out" },
21474
+ { id: "scrollCounterLine", attrs: { width: [0, 120] }, ms: 1e3, ease: "out" }
21475
+ ]
21476
+ },
21477
+ presets: [
21478
+ { name: "Revenue", values: { value: 12.5, decimals: 1, prefix: "$", suffix: "M", label: "raised so far" } },
21479
+ { name: "Percent", values: { value: 98, suffix: "%", label: "uptime", accent: [0.13, 0.77, 0.37] } }
21480
+ ]
21481
+ };
21482
+ var STEP_X = [60, 160, 260, 360];
21483
+ var scrollSteps = {
21484
+ kind: "vector",
21485
+ id: "scroll-steps",
21486
+ name: "Scroll steps",
21487
+ description: "Four step markers joined by a line that fills as you scroll, each marker lighting up as the line reaches it.",
21488
+ category: "scroll",
21489
+ trigger: "scroll",
21490
+ tags: ["interactive", "scroll", "steps", "progress", "timeline", "onboarding", "ui"],
21491
+ motion: {
21492
+ svg: `<svg viewBox="0 0 420 160" font-family="${SANS7}"><!-- {{accent: color default=#8B5CF6 label='Accent'}} {{track: color default=#E2E8F0 label='Track'}} {{text: color default=#334155 label='Text'}} --><line x1="60" y1="64" x2="360" y2="64" stroke="{{track}}" stroke-width="6" stroke-linecap="round"/><line id="scrollStepsLine" x1="60" y1="64" x2="360" y2="64" stroke="{{accent}}" stroke-width="6" stroke-linecap="round" pathLength="1" stroke-dasharray="1 1" stroke-dashoffset="1"/>${STEP_X.map(
21493
+ (x, i) => `<circle cx="${x}" cy="64" r="20" fill="#FFFFFF" stroke="{{track}}" stroke-width="4"/><circle id="scrollStepsDot${i}" cx="${x}" cy="64" r="0" fill="{{accent}}"/><text id="scrollStepsNum${i}" x="${x}" y="71" text-anchor="middle" font-size="19" font-weight="700" fill="#94A3B8">${i + 1}</text><text x="${x}" y="118" text-anchor="middle" font-size="15" font-weight="600" fill="{{text}}">{{step${i + 1}: text default='${["Sign up", "Connect", "Customise", "Launch"][i]}' label='Step ${i + 1}'}}</text>`
21494
+ ).join("")}</svg>`,
21495
+ animate: [
21496
+ { id: "scrollStepsLine", draw: true, ms: 1e3, ease: "linear" },
21497
+ ...STEP_X.flatMap((_, i) => [
21498
+ { id: `scrollStepsDot${i}`, attrs: { r: [0, 22] }, delay: Math.round(i * 1e3 / 3) - (i ? 60 : 0), ms: 160, ease: "out" },
21499
+ { id: `scrollStepsNum${i}`, attrs: { fill: ["#94A3B8", "#FFFFFF"] }, delay: Math.round(i * 1e3 / 3) - (i ? 60 : 0), ms: 160, ease: "out" }
21500
+ ])
21501
+ ]
21502
+ },
21503
+ presets: [
21504
+ { name: "Checkout", values: { step1: "Cart", step2: "Details", step3: "Payment", step4: "Done", accent: [0.06, 0.73, 0.51] } }
21505
+ ]
21506
+ };
21507
+ var scrollEffects = [scrollDraw, scrollCounter, scrollSteps];
21508
+
20627
21509
  // src/catalog/types.ts
20628
21510
  var EFFECT_CATEGORIES = [
20629
21511
  "gradient",
@@ -20654,6 +21536,8 @@ var EFFECT_CATEGORIES = [
20654
21536
  "glow",
20655
21537
  "shadow",
20656
21538
  "distortion",
21539
+ "audio",
21540
+ "scroll",
20657
21541
  "transition",
20658
21542
  "loader",
20659
21543
  "data",
@@ -20771,7 +21655,7 @@ function numberControl(name, value, min, max, label, step) {
20771
21655
  }
20772
21656
 
20773
21657
  // src/catalog/text.ts
20774
- var r110 = (n11) => Math.round(n11 * 10) / 10;
21658
+ var r110 = (n12) => Math.round(n12 * 10) / 10;
20775
21659
  function fit5(text, width, max, ems = textWidthEm(text)) {
20776
21660
  const e = Math.max(0.6, ems);
20777
21661
  const size = Math.min(max, width / e);
@@ -20788,11 +21672,11 @@ var mixHex3 = (a, b, t) => {
20788
21672
  return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
20789
21673
  };
20790
21674
  var holds = (id, attr, keys2) => keys2.map(([delay, value]) => ({ id, attrs: { [attr]: [value, value] }, delay, ms: 0 }));
20791
- var n10 = (name) => `{{${name}}}`;
21675
+ var n11 = (name) => `{{${name}}}`;
20792
21676
  var MONO = `ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
20793
21677
  var MONO_EM = 0.6;
20794
21678
  var HEAVY2 = `'Arial Black', 'Helvetica Neue', Impact, system-ui, sans-serif`;
20795
- var SANS7 = `system-ui, 'Helvetica Neue', Arial, sans-serif`;
21679
+ var SANS8 = `system-ui, 'Helvetica Neue', Arial, sans-serif`;
20796
21680
  var SCRIPT = `'Pacifico', 'Brush Script MT', 'Segoe Script', 'Snell Roundhand', cursive`;
20797
21681
  var CAP = 0.72;
20798
21682
  var typeface = (font, weight) => `{{font: text default="${font}" label='Font'}} {{weight: int min=100 max=900 default=${weight} step=100 label='Weight'}} {{inline: bool default=false label='Inline'}}`;
@@ -20836,9 +21720,9 @@ var typewriter = {
20836
21720
  motion: {
20837
21721
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Hello, world.' label='Text'}} {{paper: color default=#111827 label='Background'}} {{ink: color default=#F9FAFB label='Text colour'}} {{pace: float min=30 max=300 default=90 step=5 label='Ms per letter'}} ${typeface(MONO, 400)} --><rect width="400" height="200" fill="{{paper}}" opacity="{{backdrop}}"/><text id="typed" x="{{left}}" y="{{base}}" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" xml:space="preserve">{{text}}</text><text id="caret" x="{{end}}" y="{{base}}" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" opacity="0">\u258C</text></svg>`,
20838
21722
  animate: [
20839
- { id: "typed", write: "{{text}}", caret: "\u258C", ms: n10("typing"), ease: "linear" },
21723
+ { id: "typed", write: "{{text}}", caret: "\u258C", ms: n11("typing"), ease: "linear" },
20840
21724
  // Hidden while the writer shows its own caret, then blinking where it stopped.
20841
- ...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n10(`blink${k}`), k % 2 ? 0 : 1])])
21725
+ ...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n11(`blink${k}`), k % 2 ? 0 : 1])])
20842
21726
  ]
20843
21727
  }
20844
21728
  };
@@ -20865,8 +21749,8 @@ var scrambleText = {
20865
21749
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='ACCESS GRANTED' label='Text'}} {{glyphs: text default='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+' label='Scramble glyphs'}} {{background: color default=#05070D label='Background'}} {{ink: color default=#E2E8F0 label='Text colour'}} {{accent: color default=#22D3EE label='Accent'}} {{pace: float min=20 max=250 default=70 step=5 label='Ms per letter'}} {{glow: float min=0 max=6 default=2 step=0.5 label='Glow'}} ${typeface(MONO, 700)} --><defs><filter id="glow" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="{{glowWide}}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" filter="url(#glow)" xml:space="preserve">{{text}}</text><rect id="bar" x="{{barX}}" y="{{barY}}" width="0" height="3" fill="{{accent}}"/></svg>`,
20866
21750
  animate: [
20867
21751
  // Pure noise for a moment (the scramble runs before the change starts), then it decodes.
20868
- { id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n10("decode"), ease: "linear" },
20869
- { id: "bar", attrs: { width: [0, n10("barW")] }, delay: 500, ms: n10("decode"), ease: "linear" }
21752
+ { id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n11("decode"), ease: "linear" },
21753
+ { id: "bar", attrs: { width: [0, n11("barW")] }, delay: 500, ms: n11("decode"), ease: "linear" }
20870
21754
  ]
20871
21755
  }
20872
21756
  };
@@ -20923,13 +21807,13 @@ var kineticStagger = {
20923
21807
  {
20924
21808
  id: `l${i}`,
20925
21809
  attrs: { transform: [`translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)`, `translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`], opacity: [0, 1] },
20926
- delay: n10(`d${i}`),
21810
+ delay: n11(`d${i}`),
20927
21811
  ms: 380,
20928
21812
  ease: "out"
20929
21813
  },
20930
- { id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n10(`e${i}`), ms: 260, ease: "in-out" }
21814
+ { id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n11(`e${i}`), ms: 260, ease: "in-out" }
20931
21815
  ]).flat(),
20932
- { id: "line", attrs: { width: [0, n10("lineW")] }, delay: n10("lineAt"), ms: 500, ease: "in-out" }
21816
+ { id: "line", attrs: { width: [0, n11("lineW")] }, delay: n11("lineAt"), ms: 500, ease: "in-out" }
20933
21817
  ]
20934
21818
  }
20935
21819
  };
@@ -21001,7 +21885,7 @@ var strokeDrawText = {
21001
21885
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Draw' label='Text'}} {{background: color default=#FFFBF5 label='Background'}} {{outline: color default=#1E293B label='Outline'}} {{fill: color default=#F97316 label='Fill'}} {{width: float min=0.5 max=6 default=2 step=0.5 label='Outline width'}} {{fillOpacity: float min=0 max=1 default=1 label='Fill opacity'}} ${typeface(HEAVY2, 900)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{fill}}" fill-opacity="0" stroke="{{outline}}" stroke-width="{{width}}" stroke-linejoin="round" stroke-dasharray="0 {{dash}}">{{text}}</text></svg>`,
21002
21886
  animate: [
21003
21887
  { id: "word", attrs: { "stroke-dasharray": ["0 {{dash}}", "{{dash}} 0"] }, ms: 2e3, ease: "in-out" },
21004
- { id: "word", attrs: { "fill-opacity": [0, n10("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
21888
+ { id: "word", attrs: { "fill-opacity": [0, n11("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
21005
21889
  ]
21006
21890
  }
21007
21891
  };
@@ -21033,7 +21917,7 @@ var splitReveal = {
21033
21917
  motion: (() => {
21034
21918
  const word = `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}">{{text}}</text>`;
21035
21919
  return {
21036
- svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} ${typeface(SANS7, 800)} --><defs><clipPath id="above"><rect x="-5000" y="-5000" width="10400" height="5100"/></clipPath><clipPath id="below"><rect x="-5000" y="100" width="10400" height="5100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="{{font}}" font-size="13" letter-spacing="3" fill="{{ink}}" opacity="0">{{caption}}</text></svg>`,
21920
+ svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} ${typeface(SANS8, 800)} --><defs><clipPath id="above"><rect x="-5000" y="-5000" width="10400" height="5100"/></clipPath><clipPath id="below"><rect x="-5000" y="100" width="10400" height="5100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="{{font}}" font-size="13" letter-spacing="3" fill="{{ink}}" opacity="0">{{caption}}</text></svg>`,
21037
21921
  animate: [
21038
21922
  { id: "line", draw: true, ms: 600, ease: "in-out" },
21039
21923
  { id: "top", attrs: { transform: ["translate(0 {{shift}})", "translate(0 0)"] }, delay: 450, ms: 800, ease: "out" },
@@ -21076,7 +21960,7 @@ var highlighter = {
21076
21960
  };
21077
21961
  },
21078
21962
  motion: {
21079
- svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} ${typeface(SANS7, 700)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" opacity="0">{{text}}</text></svg>`,
21963
+ svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} ${typeface(SANS8, 700)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}" opacity="0">{{text}}</text></svg>`,
21080
21964
  animate: [
21081
21965
  { id: "phrase", attrs: { opacity: [0, 1] }, ms: 300, ease: "out" },
21082
21966
  { id: "swipe", draw: true, delay: 350, ms: 650, ease: "in-out" },
@@ -21134,7 +22018,7 @@ var wordCycle = {
21134
22018
  };
21135
22019
  },
21136
22020
  motion: {
21137
- svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} ${typeface(SANS7, 700)} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}">{{prefix}}</text><g clip-path="url(#window)"><g id="reel" transform="translate(0 -0)">${Array.from(
22021
+ svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} ${typeface(SANS8, 700)} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" fill="{{ink}}">{{prefix}}</text><g clip-path="url(#window)"><g id="reel" transform="translate(0 -0)">${Array.from(
21138
22022
  { length: CYCLE_WORDS + 1 },
21139
22023
  (_, k) => `<text x="{{wx}}" y="{{y${k}}}" textLength="{{len${k}}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{bold}}" fill="{{accent}}">{{w${k % CYCLE_WORDS}}}</text>`
21140
22024
  ).join("")}</g></g><rect id="under" x="{{wx}}" y="{{underY}}" width="{{len0}}" height="4" rx="2" fill="{{accent}}"/></svg>`,
@@ -21142,7 +22026,7 @@ var wordCycle = {
21142
22026
  const at3 = { delay: (i + 1) * SHOW_MS + i * ROLL_MS, ms: ROLL_MS, ease: "in-out" };
21143
22027
  return [
21144
22028
  { id: "reel", attrs: { transform: [`translate(0 -{{o${i}}})`, `translate(0 -{{o${i + 1}}})`] }, ...at3 },
21145
- { id: "under", attrs: { width: [n10(`len${i}`), n10(`len${i + 1}`)] }, ...at3 }
22029
+ { id: "under", attrs: { width: [n11(`len${i}`), n11(`len${i + 1}`)] }, ...at3 }
21146
22030
  ];
21147
22031
  }).flat()
21148
22032
  }
@@ -21760,7 +22644,7 @@ var dripText = {
21760
22644
  const left = 200 - width / 2;
21761
22645
  const runs = Math.min(DRIP_RUNS, Math.max(1, Math.round(width / DRIP_RUN)));
21762
22646
  const run = width / runs;
21763
- const r111 = (n11) => Math.round(n11 * 10) / 10;
22647
+ const r111 = (n12) => Math.round(n12 * 10) / 10;
21764
22648
  const edge = Number(v.stroke) / 2 / DRIP_SIZE;
21765
22649
  const tip = (DRIP_TOP + Math.max(...DRIPS2.map((d) => d.length)) * Number(v.drip) + 13 * 0.72 - DRIP_BASELINE) / DRIP_SIZE;
21766
22650
  const overhang = [edge, edge, Math.max(0.05, tip) + edge, edge].map((e) => Math.round(e * 100) / 100).join(" ");
@@ -21991,9 +22875,11 @@ var SHADER_EFFECTS = [
21991
22875
  ...glowShaders,
21992
22876
  ...shadowShaders,
21993
22877
  ...distortionShaders,
22878
+ ...audioShaders,
22879
+ ...scrollShaders,
21994
22880
  ...transitions
21995
22881
  ];
21996
- var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects].sort(
22882
+ var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects].sort(
21997
22883
  (a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
21998
22884
  );
21999
22885
  var byId = new Map(EFFECTS.map((e) => [e.id, e]));
@@ -22069,4 +22955,4 @@ export {
22069
22955
  effectControls,
22070
22956
  effectDefaults
22071
22957
  };
22072
- //# sourceMappingURL=chunk-4LKAPRFM.js.map
22958
+ //# sourceMappingURL=chunk-6GFOLHCT.js.map