@pixerate/flair 0.32.0 → 0.34.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 (39) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +3 -3
  3. package/dist/catalog/index.d.ts +4 -2
  4. package/dist/catalog/index.js +6 -2
  5. package/dist/{chunk-VLJPCY54.js → chunk-2O3RMSOE.js} +3 -3
  6. package/dist/{chunk-RS2S2K5C.js → chunk-IHUNCBMF.js} +1083 -33
  7. package/dist/chunk-IHUNCBMF.js.map +1 -0
  8. package/dist/{chunk-DA4UUSDO.js → chunk-JETRHCP3.js} +9 -277
  9. package/dist/chunk-JETRHCP3.js.map +1 -0
  10. package/dist/{chunk-AUH3PEFW.js → chunk-MOLJPWME.js} +5 -3
  11. package/dist/{chunk-AUH3PEFW.js.map → chunk-MOLJPWME.js.map} +1 -1
  12. package/dist/{chunk-X3C2VFTM.js → chunk-N5HXMQOK.js} +7 -7
  13. package/dist/{chunk-3NPBCKKO.js → chunk-UU4PHRVF.js} +6 -6
  14. package/dist/{chunk-ORAFCHX3.js → chunk-VKIDRHBG.js} +278 -2
  15. package/dist/chunk-VKIDRHBG.js.map +1 -0
  16. package/dist/cli.js +2 -2
  17. package/dist/editor/index.d.ts +1 -1
  18. package/dist/editor/index.js +13 -5
  19. package/dist/editor/index.js.map +1 -1
  20. package/dist/{effect-GGT2BUW2.js → effect-6VQS2CA5.js} +8 -8
  21. package/dist/embed/index.d.ts +1 -1
  22. package/dist/embed/index.js +4 -4
  23. package/dist/index.js +19 -19
  24. package/dist/node.d.ts +1 -1
  25. package/dist/node.js +7 -7
  26. package/dist/react/index.d.ts +1 -1
  27. package/dist/react/index.js +5 -5
  28. package/dist/svelte/index.d.ts +1 -1
  29. package/dist/svelte/index.js +5 -5
  30. package/dist/{types-c8cnPzuE.d.ts → types-B4Wjzde0.d.ts} +10 -1
  31. package/dist/vector/index.js +14 -14
  32. package/package.json +1 -1
  33. package/dist/chunk-DA4UUSDO.js.map +0 -1
  34. package/dist/chunk-ORAFCHX3.js.map +0 -1
  35. package/dist/chunk-RS2S2K5C.js.map +0 -1
  36. /package/dist/{chunk-VLJPCY54.js.map → chunk-2O3RMSOE.js.map} +0 -0
  37. /package/dist/{chunk-X3C2VFTM.js.map → chunk-N5HXMQOK.js.map} +0 -0
  38. /package/dist/{chunk-3NPBCKKO.js.map → chunk-UU4PHRVF.js.map} +0 -0
  39. /package/dist/{effect-GGT2BUW2.js.map → effect-6VQS2CA5.js.map} +0 -0
@@ -10,9 +10,11 @@ import {
10
10
  rgbToHex
11
11
  } from "./chunk-VYOMXE6I.js";
12
12
  import {
13
+ formatCount,
14
+ lerpAttr,
13
15
  morphPaths,
14
16
  morphSequence
15
- } from "./chunk-ORAFCHX3.js";
17
+ } from "./chunk-VKIDRHBG.js";
16
18
 
17
19
  // src/catalog/inputs.ts
18
20
  var EFFECT_INPUTS = ["image", "video", "svg", "text", "mask", "path", "audio"];
@@ -1545,10 +1547,10 @@ var numberTicker = {
1545
1547
  const chars = Array.from(String(v.value)).slice(0, TICKER_SLOTS);
1546
1548
  const widths = chars.map((c) => (/[0-9]/.test(c) ? 0.62 : Math.max(0.3, textWidthEm(c))) * TICKER_SIZE);
1547
1549
  const total = widths.reduce((a, b) => a + b, 0);
1548
- const fit7 = Math.min(1, 296 / Math.max(1, total));
1550
+ const fit8 = Math.min(1, 296 / Math.max(1, total));
1549
1551
  const duration = Math.round(Math.min(5, Math.max(0.2, Number(v.duration) || 1.6)) * 1e3);
1550
1552
  const stagger = Math.round(Math.min(0.5, Math.max(0, Number(v.stagger) || 0)) * 1e3);
1551
- const out = { fit: +fit7.toFixed(4), ms: duration };
1553
+ const out = { fit: +fit8.toFixed(4), ms: duration };
1552
1554
  let x = 160 - total / 2;
1553
1555
  let digit = 0;
1554
1556
  for (let i = 0; i < TICKER_SLOTS; i++) {
@@ -1679,6 +1681,19 @@ var EFFECT_CATEGORIES = [
1679
1681
  "data",
1680
1682
  "icon"
1681
1683
  ];
1684
+ var EFFECT_CATEGORY_GROUPS = [
1685
+ { name: "Backgrounds", categories: ["gradient", "organic", "pattern", "space", "3d", "weather", "water", "atmosphere"] },
1686
+ { name: "Light", categories: ["light", "lighting", "glow", "shadow", "electric"] },
1687
+ { name: "Pictures", categories: ["image", "distortion", "retro", "comic", "timewarp", "melt"] },
1688
+ { name: "Particles & FX", categories: ["overlay", "splatter", "bubble", "particles", "trail", "debris", "impact", "pyrotechnics"] },
1689
+ { name: "Type & titles", categories: ["text", "morph", "broadcast", "annotation", "sticker"] },
1690
+ { name: "Inputs", categories: ["interaction", "audio", "scroll"] },
1691
+ { name: "Interface", categories: ["ui", "loader", "icon", "data"] },
1692
+ { name: "Transitions", categories: ["transition"] }
1693
+ ];
1694
+ function categoryGroup(category) {
1695
+ return EFFECT_CATEGORY_GROUPS.find((g) => g.categories.includes(category));
1696
+ }
1682
1697
  var VIDEO_USES = ["filter", "transition", "interactive", "stills", "none"];
1683
1698
  var VECTOR_CONTROL_TYPES = ["float", "int", "bool", "color", "text"];
1684
1699
  var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
@@ -5954,8 +5969,8 @@ function packGlyph(rows) {
5954
5969
  return `ivec2(${a}, ${b})`;
5955
5970
  }
5956
5971
  var vec3Hex = (hex) => {
5957
- const n15 = parseInt(hex.slice(1), 16);
5958
- const c = [n15 >> 16, n15 >> 8 & 255, n15 & 255].map((v) => +(v / 255).toFixed(3));
5972
+ const n17 = parseInt(hex.slice(1), 16);
5973
+ const c = [n17 >> 16, n17 >> 8 & 255, n17 & 255].map((v) => +(v / 255).toFixed(3));
5959
5974
  return `vec3(${c.map((v) => Number.isInteger(v) ? `${v}.0` : String(v)).join(", ")})`;
5960
5975
  };
5961
5976
  var paletteArray = (name, hexes) => `const vec3 ${name}[${hexes.length}] = vec3[${hexes.length}](
@@ -7013,7 +7028,7 @@ var chompLoader = {
7013
7028
  var pixelEffects = [pixelText, coinSpin, spaceInvader, pixelHeartHealth, chompLoader];
7014
7029
 
7015
7030
  // src/catalog/pyro.ts
7016
- var round = (n15) => Math.round(n15 * 10) / 10;
7031
+ var round = (n17) => Math.round(n17 * 10) / 10;
7017
7032
  var fireworkBurst = {
7018
7033
  kind: "vector",
7019
7034
  id: "firework-burst",
@@ -7068,7 +7083,7 @@ var fireworkBurst = {
7068
7083
  var pyroEffects = [fireworkBurst];
7069
7084
 
7070
7085
  // src/catalog/timewarp.ts
7071
- var r111 = (n15) => Math.round(n15 * 10) / 10;
7086
+ var r111 = (n17) => Math.round(n17 * 10) / 10;
7072
7087
  var at2 = (degrees, radius) => {
7073
7088
  const a = degrees * Math.PI / 180;
7074
7089
  return [r111(100 + radius * Math.sin(a)), r111(100 - radius * Math.cos(a))];
@@ -7202,16 +7217,16 @@ function around(c, r, from, to, avoid) {
7202
7217
  if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
7203
7218
  return arc2(c[0], c[1], r, r, from, from + delta);
7204
7219
  }
7205
- function crescent(c1, r114, c2, r2_) {
7220
+ function crescent(c1, r116, c2, r2_) {
7206
7221
  const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
7207
7222
  const d = Math.hypot(dx, dy);
7208
- const a = (r114 * r114 - r2_ * r2_ + d * d) / (2 * d);
7209
- const h = Math.sqrt(Math.max(0, r114 * r114 - a * a));
7223
+ const a = (r116 * r116 - r2_ * r2_ + d * d) / (2 * d);
7224
+ const h = Math.sqrt(Math.max(0, r116 * r116 - a * a));
7210
7225
  const [px2, py2] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
7211
7226
  const i1 = [px2 - h * dy / d, py2 + h * dx / d];
7212
7227
  const i2 = [px2 + h * dy / d, py2 - h * dx / d];
7213
7228
  const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG2;
7214
- const outer = around(c1, r114, angle(c1, i1), angle(c1, i2), angle(c1, c2));
7229
+ const outer = around(c1, r116, angle(c1, i1), angle(c1, i2), angle(c1, c2));
7215
7230
  const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
7216
7231
  return polyline3([...outer, ...inner.slice(1, -1)], true);
7217
7232
  }
@@ -19738,26 +19753,26 @@ float coverage(vec2 p, float radius) {
19738
19753
  }
19739
19754
  return sum / 16.0;
19740
19755
  }`.trim();
19741
- function goldenTaps(n15) {
19742
- const taps = Array.from({ length: n15 }, (_, i) => {
19743
- const r = Math.sqrt((i + 0.5) / n15);
19756
+ function goldenTaps(n17) {
19757
+ const taps = Array.from({ length: n17 }, (_, i) => {
19758
+ const r = Math.sqrt((i + 0.5) / n17);
19744
19759
  const a = i * 2.39996;
19745
19760
  return `vec2(${(r * Math.cos(a)).toFixed(5)}, ${(r * Math.sin(a)).toFixed(5)})`;
19746
19761
  });
19747
- return `const vec2 GOLDEN${n15}[${n15}] = vec2[${n15}](${taps.join(", ")});`;
19762
+ return `const vec2 GOLDEN${n17}[${n17}] = vec2[${n17}](${taps.join(", ")});`;
19748
19763
  }
19749
- function coverageTaps(name, n15, blur) {
19764
+ function coverageTaps(name, n17, blur) {
19750
19765
  return `
19751
- ${goldenTaps(n15)}
19766
+ ${goldenTaps(n17)}
19752
19767
  float ${name}(vec2 p, float radius) {
19753
19768
  float sum = 0.0;
19754
19769
  if (gMaskPlace.w < 0.5) {
19755
- for (int i = 0; i < ${n15}; i++) sum += shapeAt(p + radius * GOLDEN${n15}[i], radius * ${blur.toFixed(2)});
19770
+ for (int i = 0; i < ${n17}; i++) sum += shapeAt(p + radius * GOLDEN${n17}[i], radius * ${blur.toFixed(2)});
19756
19771
  } else {
19757
19772
  float lod = log2(max(radius * ${blur.toFixed(2)} * gMaskPlace.z, 1.0));
19758
- for (int i = 0; i < ${n15}; i++) sum += shapeLod(p + radius * GOLDEN${n15}[i], lod);
19773
+ for (int i = 0; i < ${n17}; i++) sum += shapeLod(p + radius * GOLDEN${n17}[i], lod);
19759
19774
  }
19760
- return sum / ${n15}.0;
19775
+ return sum / ${n17}.0;
19761
19776
  }`.trim();
19762
19777
  }
19763
19778
  var LITE_TAPS = 10;
@@ -24982,7 +24997,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24982
24997
  var waterShaders = [oceanWaves, underwaterScene, pondRipples, waterfall];
24983
24998
 
24984
24999
  // src/catalog/text.ts
24985
- var r112 = (n15) => Math.round(n15 * 10) / 10;
25000
+ var r112 = (n17) => Math.round(n17 * 10) / 10;
24986
25001
  function fit5(text, width, max, ems = textWidthEm(text)) {
24987
25002
  const e = Math.max(0.6, ems);
24988
25003
  const size = Math.min(max, width / e);
@@ -25446,7 +25461,7 @@ var glitchText = {
25446
25461
  };
25447
25462
  },
25448
25463
  motion: (() => {
25449
- const word = (paint) => `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" ${paint}>{{text}}</text>`;
25464
+ const word = (paint2) => `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" ${paint2}>{{text}}</text>`;
25450
25465
  const jumps = (id, value) => GLITCH.map((key, k) => ({ id, attrs: { transform: [value(k), value(k)] }, delay: key.at, ms: 0 }));
25451
25466
  return {
25452
25467
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='GLITCH' label='Text'}} {{background: color default=#07070B label='Background'}} {{ink: color default=#F5F5F5 label='Text colour'}} {{left: color default=#FF0040 label='Red copy'}} {{right: color default=#00F0FF label='Cyan copy'}} {{amount: float min=0 max=3 default=1 step=0.1 label='Intensity'}} ${typeface(HEAVY2, 900)} --><defs>${[0, 1, 2, 3].map((b) => `<clipPath id="slice${b}"><rect x="-5000" y="{{cut${b}}}" width="10400" height="{{band${b}}}"/></clipPath>`).join("")}</defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g id="red" style="mix-blend-mode:screen">${word(`fill="{{left}}"`)}</g><g id="cyan" style="mix-blend-mode:screen">${word(`fill="{{right}}"`)}</g>${[0, 1, 2, 3].map((b) => `<g clip-path="url(#slice${b})"><g id="s${b}">${word(`fill="{{ink}}"`)}</g></g>`).join("")}</svg>`,
@@ -25500,7 +25515,7 @@ var extrudedText = {
25500
25515
  };
25501
25516
  },
25502
25517
  motion: (() => {
25503
- const word = (paint) => `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" stroke-linejoin="round" ${paint}`;
25518
+ const word = (paint2) => `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" stroke-linejoin="round" ${paint2}`;
25504
25519
  const layers = Array.from({ length: LAYERS }, (_, k) => LAYERS - k);
25505
25520
  return {
25506
25521
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='RETRO' label='Text'}} {{background: color default=#FFE8C2 label='Background'}} {{face: color default=#FFD23F label='Face'}} {{outline: color default=#1A1423 label='Outline'}} {{side: color default=#EE4266 label='Extrude'}} {{shade: color default=#3B1F2B label='Shadow'}} {{depth: float min=0.2 max=2 default=1 step=0.1 label='Depth'}} ${typeface(HEAVY2, 900)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/>${layers.map((i) => `<text id="e${i}" transform="translate(0 0)" ${word(`fill="{{fill${i}}}" stroke="{{fill${i}}}" stroke-width="2"`)}>{{text}}</text>`).join("")}<text ${word(`fill="{{face}}" stroke="{{outline}}" stroke-width="2.5" paint-order="stroke"`)}>{{text}}</text></svg>`,
@@ -27181,7 +27196,7 @@ var dripText = {
27181
27196
  const left = 200 - width / 2;
27182
27197
  const runs = Math.min(DRIP_RUNS, Math.max(1, Math.round(width / DRIP_RUN)));
27183
27198
  const run = width / runs;
27184
- const r114 = (n15) => Math.round(n15 * 10) / 10;
27199
+ const r116 = (n17) => Math.round(n17 * 10) / 10;
27185
27200
  const edge = Number(v.stroke) / 2 / DRIP_SIZE;
27186
27201
  const tip = (DRIP_TOP + Math.max(...DRIPS2.map((d) => d.length)) * Number(v.drip) + 13 * 0.72 - DRIP_BASELINE) / DRIP_SIZE;
27187
27202
  const overhang = [edge, edge, Math.max(0.05, tip) + edge, edge].map((e) => Math.round(e * 100) / 100).join(" ");
@@ -27195,15 +27210,15 @@ var dripText = {
27195
27210
  overhang,
27196
27211
  ...Object.fromEntries(
27197
27212
  DRIP_SLOTS.flatMap((d) => {
27198
- const x = r114(left + (d.run + d.at) * run);
27199
- const length = r114(d.length * Number(v.drip));
27213
+ const x = r116(left + (d.run + d.at) * run);
27214
+ const length = r116(d.length * Number(v.drip));
27200
27215
  return [
27201
27216
  [`show${d.i}`, d.run < runs ? "inline" : "none"],
27202
27217
  [`x${d.i}`, x],
27203
- [`left${d.i}`, r114(x - d.width / 2)],
27204
- [`shine${d.i}`, r114(x - d.width * 0.25)],
27218
+ [`left${d.i}`, r116(x - d.width / 2)],
27219
+ [`shine${d.i}`, r116(x - d.width * 0.25)],
27205
27220
  [`len${d.i}`, length],
27206
- [`tip${d.i}`, r114(DRIP_TOP + length)]
27221
+ [`tip${d.i}`, r116(DRIP_TOP + length)]
27207
27222
  ];
27208
27223
  })
27209
27224
  )
@@ -27211,8 +27226,8 @@ var dripText = {
27211
27226
  },
27212
27227
  motion: (() => {
27213
27228
  const word = `<text x="200" y="${DRIP_BASELINE}" text-anchor="middle" textLength="{{width}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="${DRIP_SIZE}" font-weight="{{weight}}" stroke-linejoin="round"`;
27214
- const drips = (layer, paint) => DRIP_SLOTS.map(
27215
- (d) => `<g display="{{show${d.i}}}"><rect id="${layer}r${d.i}" x="{{left${d.i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${paint}/><circle id="${layer}c${d.i}" cx="{{x${d.i}}}" cy="${DRIP_TOP}" r="${d.width * 0.72}" ${paint}/></g>`
27229
+ const drips = (layer, paint2) => DRIP_SLOTS.map(
27230
+ (d) => `<g display="{{show${d.i}}}"><rect id="${layer}r${d.i}" x="{{left${d.i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${paint2}/><circle id="${layer}c${d.i}" cx="{{x${d.i}}}" cy="${DRIP_TOP}" r="${d.width * 0.72}" ${paint2}/></g>`
27216
27231
  ).join("");
27217
27232
  const outline4 = `fill="{{outline}}" stroke="{{outline}}" stroke-width="{{stroke}}"`;
27218
27233
  const pop2 = (from, to) => [`translate(200 100) scale(${from}) translate(-200 -100)`, `translate(200 100) scale(${to}) translate(-200 -100)`];
@@ -27346,6 +27361,1038 @@ var vectorEffects = [
27346
27361
  uploadCard
27347
27362
  ];
27348
27363
 
27364
+ // src/catalog/data.ts
27365
+ var r114 = (v) => Math.round(v * 10) / 10;
27366
+ var r212 = (v) => Math.round(v * 100) / 100;
27367
+ var n15 = (name) => `{{${name}}}`;
27368
+ var SANS12 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
27369
+ function fit7(text, width, max) {
27370
+ const e = Math.max(0.6, textWidthEm(String(text)));
27371
+ const size = Math.min(max, width / e);
27372
+ return { size: r114(size), length: r114(e * size) };
27373
+ }
27374
+ function fitLabel(text, width, max, min) {
27375
+ const whole = String(text ?? "");
27376
+ const f = fit7(whole, width, max);
27377
+ if (f.size >= min) return { text: whole, ...f };
27378
+ const letters = Array.from(whole);
27379
+ while (letters.length > 1 && Math.max(0.6, textWidthEm(`${letters.join("").trimEnd()}\u2026`)) * min > width) letters.pop();
27380
+ const cut = `${letters.join("").trimEnd()}\u2026`;
27381
+ return { text: cut, ...fit7(cut, width, max) };
27382
+ }
27383
+ var paint = (v) => isCurrentColor(v) ? "currentColor" : Array.isArray(v) ? rgbToHex(v) : "#000000";
27384
+ var formatSafe = (v) => String(v ?? "").replace(/[{}]/g, "");
27385
+ var DATA_PALETTE = ["#6366F1", "#22C55E", "#F59E0B", "#EC4899", "#0EA5E9", "#A855F7"];
27386
+ var palette = (count = DATA_PALETTE.length) => DATA_PALETTE.slice(0, count).map((hex, i) => `{{c${i + 1}: color default=${hex} label='Colour ${i + 1}'}}`).join(" ");
27387
+ var PALETTE_PRESETS = [
27388
+ { name: "Ocean", values: { c1: [0.03, 0.4, 0.62], c2: [0.05, 0.65, 0.91], c3: [0.22, 0.74, 0.97], c4: [0.49, 0.83, 0.99], c5: [0.08, 0.72, 0.65], c6: [0.37, 0.92, 0.83] } },
27389
+ { name: "Sunset", values: { c1: [0.94, 0.27, 0.27], c2: [0.98, 0.45, 0.09], c3: [0.98, 0.75, 0.14], c4: [0.93, 0.29, 0.6], c5: [0.66, 0.33, 0.97], c6: [0.4, 0.4, 0.95] } }
27390
+ ];
27391
+ function parseNumberList(text, max) {
27392
+ return String(text ?? "").split(/[,;\s]+/).filter((t) => t !== "").slice(0, max).map((token) => {
27393
+ const cleaned = token.replace(/[^0-9.eE+-]/g, "");
27394
+ const value = /\d/.test(cleaned) ? Number(cleaned) : NaN;
27395
+ return Number.isFinite(value) ? Math.max(-1e9, Math.min(1e9, value)) : NaN;
27396
+ });
27397
+ }
27398
+ function parseLabels(text, max) {
27399
+ return String(text ?? "").split(/[,;\n]/).map((s) => s.trim()).slice(0, max);
27400
+ }
27401
+ function parseShares(text, max, fallback) {
27402
+ const values = parseNumberList(text, max).map((v) => Number.isFinite(v) && v > 0 ? v : 0);
27403
+ return values.some((v) => v > 0) ? values : [...fallback];
27404
+ }
27405
+ function decimalsOf(values) {
27406
+ let most = 0;
27407
+ for (const v of values) {
27408
+ if (!Number.isFinite(v) || Number.isInteger(v)) continue;
27409
+ const s = String(v);
27410
+ most = Math.max(most, s.includes("e") ? 2 : s.split(".")[1]?.length ?? 0);
27411
+ }
27412
+ return Math.min(2, most);
27413
+ }
27414
+ function parseRace(text, maxRows, maxStates, fallback) {
27415
+ const rows = [];
27416
+ for (const line of String(text ?? "").split(/[;\n]+/)) {
27417
+ if (!line.trim()) continue;
27418
+ const colon = line.indexOf(":");
27419
+ const name = colon >= 0 ? line.slice(0, colon).trim() : `#${rows.length + 1}`;
27420
+ const raw = parseNumberList(colon >= 0 ? line.slice(colon + 1) : line, maxStates);
27421
+ if (raw.some((v) => Number.isFinite(v))) rows.push({ name, raw });
27422
+ if (rows.length === maxRows) break;
27423
+ }
27424
+ if (!rows.length) return fallback.map((r) => ({ name: r.name, values: [...r.values] }));
27425
+ const states = Math.max(...rows.map((r) => r.raw.length));
27426
+ return rows.map(({ name, raw }) => {
27427
+ let last = raw.find((v) => Number.isFinite(v)) ?? 0;
27428
+ const values = Array.from({ length: states }, (_, k) => {
27429
+ const v = raw[k];
27430
+ if (v !== void 0 && Number.isFinite(v)) last = Math.max(0, v);
27431
+ return last;
27432
+ });
27433
+ return { name, values };
27434
+ });
27435
+ }
27436
+ function parseSeries(text, maxRows, maxPoints, fallback) {
27437
+ const rows = String(text ?? "").split(/[;\n]+/).map((line) => parseNumberList(line, maxPoints)).filter((row) => row.some((v) => Number.isFinite(v))).slice(0, maxRows).map((row) => row.map((v) => Number.isFinite(v) && v > 0 ? v : 0));
27438
+ if (!rows.length || rows.every((r) => r.every((v) => v === 0))) return fallback.map((r) => [...r]);
27439
+ const points = Math.max(2, ...rows.map((r) => r.length));
27440
+ return rows.map((r) => Array.from({ length: points }, (_, j) => r[Math.min(j, r.length - 1)]));
27441
+ }
27442
+ var SLOTS3 = 6;
27443
+ var LEGEND = { swatch: 158, label: 174, right: 250, pitch: 22 };
27444
+ function legendValues(labels, fractions, at4, cy) {
27445
+ const shown = fractions.map((f) => f > 0);
27446
+ const count = shown.filter(Boolean).length;
27447
+ const out = {};
27448
+ let row = 0;
27449
+ for (let i = 0; i < SLOTS3; i++) {
27450
+ const f = fractions[i] ?? 0;
27451
+ const label = labels[i] ?? "";
27452
+ const { text, size, length } = fitLabel(label, LEGEND.right - LEGEND.label - 32, 11, 8);
27453
+ out[`show${i}`] = f > 0 ? "inline" : "none";
27454
+ out[`ly${i}`] = r114(cy + ((f > 0 ? row : 0) - (count - 1) / 2) * LEGEND.pitch);
27455
+ out[`l${i}`] = text;
27456
+ out[`ls${i}`] = size;
27457
+ out[`ll${i}`] = length;
27458
+ out[`pct${i}`] = Math.round(f * 100);
27459
+ out[`at${i}`] = at4[i] ?? 0;
27460
+ if (f > 0) row++;
27461
+ }
27462
+ return out;
27463
+ }
27464
+ var legendMarkup = (prefix) => Array.from(
27465
+ { length: SLOTS3 },
27466
+ (_, i) => `<g id="${prefix}Row${i}" display="{{show${i}}}" opacity="0" transform="translate(8 {{ly${i}}})"><rect x="${LEGEND.swatch}" y="-5" width="10" height="10" rx="3" fill="{{c${i + 1}}}"/><text x="${LEGEND.label}" y="4" font-size="{{ls${i}}}" textLength="{{ll${i}}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}">{{l${i}}}</text><text id="${prefix}Pct${i}" x="${LEGEND.right}" y="4" text-anchor="end" font-size="11" font-weight="700" fill="{{ink}}">0%</text></g>`
27467
+ ).join("");
27468
+ var legendAnimations = (prefix) => Array.from({ length: SLOTS3 }, (_, i) => [
27469
+ { id: `${prefix}Row${i}`, attrs: { opacity: [0, 1], transform: [`translate(8 {{ly${i}}})`, `translate(0 {{ly${i}}})`] }, delay: n15(`at${i}`), ms: 320, ease: "out" },
27470
+ { id: `${prefix}Pct${i}`, count: [0, n15(`pct${i}`)], format: "{n}%", delay: n15(`at${i}`), ms: 600, ease: "out" }
27471
+ ]).flat();
27472
+ var DONUT = { cx: 80, cy: 80, r: 50, width: 22, start: 150, sweep: 1400 };
27473
+ var DONUT_DEFAULT = [42, 28, 18, 12];
27474
+ var donutChart = {
27475
+ kind: "vector",
27476
+ id: "donut-chart",
27477
+ name: "Donut chart",
27478
+ description: "Segments sweeping round a ring one after another, a legend giving each share, and the total counting up in the middle.",
27479
+ category: "data",
27480
+ tags: ["chart", "donut", "share", "dashboard", "report", "explainer"],
27481
+ hold: 1600,
27482
+ presets: [...PALETTE_PRESETS, { name: "Budget", values: { values: "1200,800,450,300,250", labels: "Rent,Food,Travel,Fun,Other", caption: "per month" } }],
27483
+ derived: (v) => {
27484
+ const values = parseShares(v.values, SLOTS3, DONUT_DEFAULT);
27485
+ const total = values.reduce((a, b) => a + b, 0);
27486
+ const fractions = values.map((x) => x / total);
27487
+ const gap = fractions.filter((f) => f > 0).length > 1 ? 5e-3 : 0;
27488
+ const out = {};
27489
+ const at4 = [];
27490
+ let cum = 0;
27491
+ for (let i = 0; i < SLOTS3; i++) {
27492
+ const f = fractions[i] ?? 0;
27493
+ at4.push(Math.round(DONUT.start + cum * DONUT.sweep));
27494
+ out[`rot${i}`] = r212(-90 + cum * 360);
27495
+ out[`frac${i}`] = Math.min(1, Math.max(1e-3, +(f - gap).toFixed(4)));
27496
+ out[`ms${i}`] = Math.max(60, Math.round(f * DONUT.sweep));
27497
+ cum += f;
27498
+ }
27499
+ const dec = decimalsOf(values);
27500
+ const totalSize = fit7(formatCount(total, { decimals: dec }), 64, 26).size;
27501
+ const caption = fitLabel(v.caption, 58, 9.5, 7.5);
27502
+ return {
27503
+ ...legendValues(parseLabels(v.labels, SLOTS3), fractions, at4, DONUT.cy),
27504
+ ...out,
27505
+ total: +total.toFixed(dec),
27506
+ dec,
27507
+ totalSize,
27508
+ totalY: r114(DONUT.cy + totalSize * 0.35 - (String(v.caption).trim() ? 5 : 0)),
27509
+ captionY: r114(DONUT.cy + totalSize * 0.35 + 9),
27510
+ captionText: caption.text,
27511
+ captionSize: caption.size,
27512
+ captionLength: caption.length,
27513
+ countMs: DONUT.start + DONUT.sweep
27514
+ };
27515
+ },
27516
+ motion: {
27517
+ svg: `<svg viewBox="0 0 260 160" font-family="${SANS12}"><!-- {{values: text default='42,28,18,12' label='Values (comma-separated)'}} {{labels: text default='Direct,Social,Search,Email' label='Labels (comma-separated)'}} {{caption: text default='visits' label='Caption'}} {{ink: color default=#1E293B label='Text'}} {{track: color default=#E2E8F0 label='Track'}} ${palette()} --><circle cx="${DONUT.cx}" cy="${DONUT.cy}" r="${DONUT.r}" fill="none" stroke="{{track}}" stroke-width="${DONUT.width}"/>${Array.from(
27518
+ { length: SLOTS3 },
27519
+ (_, i) => `<g display="{{show${i}}}"><circle id="donutChartSeg${i}" cx="${DONUT.cx}" cy="${DONUT.cy}" r="${DONUT.r}" fill="none" stroke="{{c${i + 1}}}" stroke-width="${DONUT.width}" transform="rotate({{rot${i}}} ${DONUT.cx} ${DONUT.cy})"/></g>`
27520
+ ).join(
27521
+ ""
27522
+ )}<text id="donutChartTotal" x="${DONUT.cx}" y="{{totalY}}" text-anchor="middle" font-size="{{totalSize}}" font-weight="800" fill="{{ink}}">0</text><text x="${DONUT.cx}" y="{{captionY}}" text-anchor="middle" font-size="{{captionSize}}" textLength="{{captionLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.6">{{captionText}}</text>${legendMarkup("donutChart")}</svg>`,
27523
+ animate: [
27524
+ ...Array.from({ length: SLOTS3 }, (_, i) => ({ id: `donutChartSeg${i}`, draw: [0, n15(`frac${i}`)], delay: n15(`at${i}`), ms: n15(`ms${i}`), ease: "linear" })),
27525
+ { id: "donutChartTotal", count: [0, n15("total")], decimals: n15("dec"), ms: n15("countMs"), ease: "out" },
27526
+ ...legendAnimations("donutChart")
27527
+ ]
27528
+ }
27529
+ };
27530
+ var PIE = { cx: 80, cy: 80, r: 58, pop: 7, start: 150, stagger: 230 };
27531
+ var PIE_DEFAULT = [38, 26, 20, 16];
27532
+ var pieMove = (dx, dy, s) => `translate(${dx} ${dy}) translate(${PIE.cx} ${PIE.cy}) scale(${s}) translate(${-PIE.cx} ${-PIE.cy})`;
27533
+ function wedge(a, b) {
27534
+ const { cx, cy, r } = PIE;
27535
+ if (b - a <= 0) return `M${cx} ${cy}Z`;
27536
+ if (b - a >= 0.9999) return `M${cx} ${cy - r} A${r} ${r} 0 1 1 ${cx} ${cy + r} A${r} ${r} 0 1 1 ${cx} ${cy - r}Z`;
27537
+ const at4 = (f) => `${r212(cx + r * Math.sin(f * 2 * Math.PI))} ${r212(cy - r * Math.cos(f * 2 * Math.PI))}`;
27538
+ return `M${cx} ${cy} L${at4(a)} A${r} ${r} 0 ${b - a > 0.5 ? 1 : 0} 1 ${at4(b)}Z`;
27539
+ }
27540
+ var pieChart = {
27541
+ kind: "vector",
27542
+ id: "pie-chart",
27543
+ name: "Pie chart",
27544
+ description: "Pie slices popping out one at a time and settling into place, each with its percentage, beside a legend.",
27545
+ category: "data",
27546
+ tags: ["chart", "pie", "share", "percentage", "dashboard", "report"],
27547
+ hold: 1600,
27548
+ presets: [...PALETTE_PRESETS, { name: "Survey", values: { values: "54,31,15", labels: "Yes,No,Not sure" } }],
27549
+ derived: (v) => {
27550
+ const values = parseShares(v.values, SLOTS3, PIE_DEFAULT);
27551
+ const total = values.reduce((a, b) => a + b, 0);
27552
+ const fractions = values.map((x) => x / total);
27553
+ const out = {};
27554
+ const at4 = [];
27555
+ const ink = paint(v.ink);
27556
+ let cum = 0;
27557
+ for (let i = 0; i < SLOTS3; i++) {
27558
+ const f = fractions[i] ?? 0;
27559
+ const mid = (cum + f / 2) * 2 * Math.PI;
27560
+ const inside = f >= 0.08;
27561
+ out[`tshow${i}`] = f >= 0.04 ? "inline" : "none";
27562
+ const radius = inside ? PIE.r * 0.62 : PIE.r + 14;
27563
+ at4.push(PIE.start + i * PIE.stagger);
27564
+ out[`d${i}`] = wedge(cum, cum + f);
27565
+ out[`ox${i}`] = r212(PIE.pop * Math.sin(mid));
27566
+ out[`oy${i}`] = r212(-PIE.pop * Math.cos(mid));
27567
+ out[`tx${i}`] = r114(PIE.cx + radius * Math.sin(mid));
27568
+ out[`ty${i}`] = r114(PIE.cy - radius * Math.cos(mid) + 4);
27569
+ out[`tf${i}`] = inside ? paint(v.onSlice) : ink;
27570
+ out[`tl${i}`] = PIE.start + i * PIE.stagger + 260;
27571
+ cum += f;
27572
+ }
27573
+ return { ...legendValues(parseLabels(v.labels, SLOTS3), fractions, at4, PIE.cy), ...out };
27574
+ },
27575
+ motion: {
27576
+ svg: `<svg viewBox="0 0 260 160" font-family="${SANS12}"><!-- {{values: text default='38,26,20,16' label='Values (comma-separated)'}} {{labels: text default='Product,Services,Support,Other' label='Labels (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} {{onSlice: color default=#FFFFFF label='Labels on slices'}} {{edge: color default=#FFFFFF label='Slice edges'}} ${palette()} -->${Array.from(
27577
+ { length: SLOTS3 },
27578
+ (_, i) => `<g id="pieChartSlice${i}" display="{{show${i}}}" transform="${pieMove(0, 0, 0)}"><path d="{{d${i}}}" fill="{{c${i + 1}}}" stroke="{{edge}}" stroke-width="2" stroke-linejoin="round"/></g>`
27579
+ ).join("")}${Array.from(
27580
+ { length: SLOTS3 },
27581
+ (_, i) => `<text id="pieChartLabel${i}" display="{{tshow${i}}}" x="{{tx${i}}}" y="{{ty${i}}}" text-anchor="middle" font-size="11" font-weight="700" fill="{{tf${i}}}" opacity="0">0%</text>`
27582
+ ).join("")}${legendMarkup("pieChart")}</svg>`,
27583
+ animate: [
27584
+ ...Array.from({ length: SLOTS3 }, (_, i) => [
27585
+ { id: `pieChartSlice${i}`, attrs: { transform: [pieMove(0, 0, 0), pieMove(`{{ox${i}}}`, `{{oy${i}}}`, 1.06)] }, delay: n15(`at${i}`), ms: 260, ease: "out" },
27586
+ { id: `pieChartSlice${i}`, attrs: { transform: [pieMove(`{{ox${i}}}`, `{{oy${i}}}`, 1.06), pieMove(0, 0, 1)] }, delay: n15(`tl${i}`), ms: 300, ease: "in-out" },
27587
+ { id: `pieChartLabel${i}`, attrs: { opacity: [0, 1] }, delay: n15(`tl${i}`), ms: 200 },
27588
+ { id: `pieChartLabel${i}`, count: [0, n15(`pct${i}`)], format: "{n}%", delay: n15(`tl${i}`), ms: 500, ease: "out" }
27589
+ ]).flat(),
27590
+ ...legendAnimations("pieChart")
27591
+ ]
27592
+ }
27593
+ };
27594
+ var GAUGE = { cx: 120, cy: 112, r: 84, band: 14 };
27595
+ var gaugeAt = (f, r) => `${r212(GAUGE.cx - r * Math.cos(f * Math.PI))} ${r212(GAUGE.cy - r * Math.sin(f * Math.PI))}`;
27596
+ var gaugeArc = (a, b) => b - a <= 5e-4 ? `M${gaugeAt(a, GAUGE.r)}` : `M${gaugeAt(a, GAUGE.r)} A${GAUGE.r} ${GAUGE.r} 0 0 1 ${gaugeAt(b, GAUGE.r)}`;
27597
+ var needle = (deg) => `rotate(${deg} ${GAUGE.cx} ${GAUGE.cy})`;
27598
+ var gauge = {
27599
+ kind: "vector",
27600
+ id: "gauge",
27601
+ name: "Gauge",
27602
+ description: "A speedometer gauge: its coloured band draws on, the needle sweeps past the value and settles, and the reading counts up beneath.",
27603
+ category: "data",
27604
+ tags: ["gauge", "speedometer", "meter", "kpi", "dashboard", "score"],
27605
+ hold: 1600,
27606
+ presets: [
27607
+ { name: "Speed", values: { value: 88, max: 160, unit: " km/h", label: "Speed", warn: 70, danger: 85 } },
27608
+ { name: "Dark", values: { ink: [0.94, 0.95, 0.97], needleColor: [0.94, 0.95, 0.97], hub: [0.06, 0.07, 0.1] } }
27609
+ ],
27610
+ derived: (v) => {
27611
+ const max = Math.max(1e-3, Number(v.max) || 100);
27612
+ const value = Math.min(max, Math.max(0, Number(v.value) || 0));
27613
+ const f = value / max;
27614
+ const warn = Math.min(1, Math.max(0, Number(v.warn) / 100));
27615
+ const danger = Math.min(1, Math.max(warn, Number(v.danger) / 100));
27616
+ const dec = decimalsOf([value]);
27617
+ const unit = formatSafe(v.unit);
27618
+ const reading = fit7(`${formatCount(value, { decimals: dec })}${unit}`, 150, 26);
27619
+ const label = fitLabel(v.label, 150, 10, 8);
27620
+ const ends = [0, max].map((x) => fit7(formatCount(x, { decimals: decimalsOf([x]) }), 30, 10).size);
27621
+ return {
27622
+ band0: gaugeArc(0, warn),
27623
+ band1: gaugeArc(warn, danger),
27624
+ band2: gaugeArc(danger, 1),
27625
+ over: needle(r114(180 * Math.min(1, f + 0.06))),
27626
+ angle: needle(r114(180 * f)),
27627
+ reading: +value.toFixed(dec),
27628
+ dec,
27629
+ unit,
27630
+ readingSize: reading.size,
27631
+ labelText: label.text,
27632
+ labelSize: label.size,
27633
+ labelLength: label.length,
27634
+ maxText: formatCount(max, { decimals: decimalsOf([max]) }),
27635
+ maxSize: ends[1]
27636
+ };
27637
+ },
27638
+ motion: {
27639
+ svg: `<svg viewBox="0 0 240 170" font-family="${SANS12}"><!-- {{value: float min=0 max=10000 default=72 step=1 label='Value'}} {{max: float min=1 max=10000 default=100 step=1 label='Scale maximum'}} {{unit: text default='%' label='Unit'}} {{label: text default='Performance' label='Label'}} {{warn: float min=0 max=100 default=60 step=1 label='Amber from (% of scale)'}} {{danger: float min=0 max=100 default=85 step=1 label='Red from (% of scale)'}} {{low: color default=#22C55E label='Low band'}} {{mid: color default=#F59E0B label='Middle band'}} {{high: color default=#EF4444 label='High band'}} {{needleColor: color default=#0F172A label='Needle'}} {{hub: color default=#FFFFFF label='Hub centre'}} {{ink: color default=#1E293B label='Text'}} --><path id="gaugeBand0" d="{{band0}}" fill="none" stroke="{{low}}" stroke-width="${GAUGE.band}"/><path id="gaugeBand1" d="{{band1}}" fill="none" stroke="{{mid}}" stroke-width="${GAUGE.band}"/><path id="gaugeBand2" d="{{band2}}" fill="none" stroke="{{high}}" stroke-width="${GAUGE.band}"/><g stroke="{{ink}}" stroke-opacity="0.35" stroke-linecap="round">${Array.from({ length: 11 }, (_, k) => `<path d="M${gaugeAt(k / 10, 70)} L${gaugeAt(k / 10, k % 5 ? 65 : 61)}" stroke-width="${k % 5 ? 1.5 : 2.5}"/>`).join("")}</g><text x="${GAUGE.cx - GAUGE.r}" y="130" text-anchor="middle" font-size="10" fill="{{ink}}" fill-opacity="0.6">0</text><text x="${GAUGE.cx + GAUGE.r}" y="130" text-anchor="middle" font-size="{{maxSize}}" fill="{{ink}}" fill-opacity="0.6">{{maxText}}</text><g id="gaugeNeedle" transform="${needle(0)}"><path d="M${GAUGE.cx} ${GAUGE.cy - 4} L${GAUGE.cx - GAUGE.r + 12} ${GAUGE.cy} L${GAUGE.cx} ${GAUGE.cy + 4}Z" fill="{{needleColor}}"/></g><circle cx="${GAUGE.cx}" cy="${GAUGE.cy}" r="8" fill="{{needleColor}}"/><circle cx="${GAUGE.cx}" cy="${GAUGE.cy}" r="3" fill="{{hub}}"/><text id="gaugeValue" x="${GAUGE.cx}" y="148" text-anchor="middle" font-size="{{readingSize}}" font-weight="800" fill="{{ink}}">0</text><text x="${GAUGE.cx}" y="164" text-anchor="middle" font-size="{{labelSize}}" textLength="{{labelLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.6">{{labelText}}</text></svg>`,
27640
+ animate: [
27641
+ ...[0, 1, 2].map((k) => ({ id: `gaugeBand${k}`, draw: true, delay: k * 140, ms: 320, ease: k === 0 ? "out" : "linear" })),
27642
+ { id: "gaugeNeedle", attrs: { transform: [needle(0), "{{over}}"] }, delay: 300, ms: 900, ease: "out" },
27643
+ { id: "gaugeNeedle", attrs: { transform: ["{{over}}", "{{angle}}"] }, delay: 1200, ms: 450, ease: "in-out" },
27644
+ { id: "gaugeValue", count: [0, n15("reading")], decimals: n15("dec"), format: "{n}{{unit}}", delay: 300, ms: 1300, ease: "out" }
27645
+ ]
27646
+ }
27647
+ };
27648
+ var RACE_ROWS = 6;
27649
+ var RACE_STATES = 5;
27650
+ var RACE = { left: 74, top: 12, pitch: 28, height: 22, width: 320, value: 10, intro: 900, move: 1e3, rest: 700 };
27651
+ var RACE_DEFAULT = [
27652
+ { name: "Lumen", values: [12, 28, 41, 66] },
27653
+ { name: "Pixel", values: [30, 36, 48, 54] },
27654
+ { name: "Vertex", values: [22, 40, 39, 71] },
27655
+ { name: "Shader", values: [8, 20, 52, 60] },
27656
+ { name: "Raster", values: [18, 24, 30, 35] },
27657
+ { name: "Vector", values: [26, 30, 44, 49] }
27658
+ ];
27659
+ var raceTime = (k) => RACE.intro + RACE.rest + (k - 1) * (RACE.move + RACE.rest);
27660
+ var barRace = {
27661
+ kind: "vector",
27662
+ id: "bar-race",
27663
+ name: "Bar race",
27664
+ description: "A bar chart race: bars stretch and overtake each other, swapping places as the period label ticks through the years.",
27665
+ category: "data",
27666
+ tags: ["chart", "race", "ranking", "bars", "timeline", "explainer"],
27667
+ hold: 1800,
27668
+ presets: [
27669
+ ...PALETTE_PRESETS,
27670
+ { name: "Browsers", values: { rows: "Chrome:20,45,62,65;Firefox:28,30,22,8;Safari:5,8,15,19;Edge:0,2,4,5;Opera:3,2,2,2", periods: "2009,2012,2016,2020" } }
27671
+ ],
27672
+ derived: (v) => {
27673
+ const rows = parseRace(v.rows, RACE_ROWS, RACE_STATES, RACE_DEFAULT);
27674
+ const states = Math.max(1, rows[0].values.length);
27675
+ const all = rows.flatMap((r) => r.values);
27676
+ const dec = decimalsOf(all);
27677
+ const valueWidth = Math.max(...all.map((x) => textWidthEm(formatCount(x, { decimals: dec })))) * RACE.value;
27678
+ const barWidth = Math.max(40, RACE.width - RACE.left - 10 - valueWidth);
27679
+ const periods = parseLabels(v.periods, RACE_STATES);
27680
+ const end = states > 1 ? raceTime(states - 1) + RACE.move : RACE.intro;
27681
+ const out = { dec };
27682
+ const ranks = Array.from({ length: RACE_STATES }, (_, k) => {
27683
+ const s = Math.min(k, states - 1);
27684
+ const order = rows.map((r, i) => [i, r.values[s]]).sort((a, b) => b[1] - a[1] || a[0] - b[0]);
27685
+ const rank = new Map(order.map(([i], place) => [i, place]));
27686
+ const top = Math.max(1e-9, order[0][1]);
27687
+ return { s, rank, top };
27688
+ });
27689
+ for (let i = 0; i < RACE_ROWS; i++) {
27690
+ const row = rows[i];
27691
+ const name = fitLabel(row?.name ?? "", RACE.left - 12, 11, 8);
27692
+ out[`show${i}`] = row ? "inline" : "none";
27693
+ out[`nm${i}`] = name.text;
27694
+ out[`ns${i}`] = name.size;
27695
+ out[`nl${i}`] = name.length;
27696
+ out[`in${i}`] = i * 70;
27697
+ for (let k = 0; k < RACE_STATES; k++) {
27698
+ const { s, rank, top } = ranks[k];
27699
+ const value = row ? row.values[s] : 0;
27700
+ const w = r114(value / top * barWidth);
27701
+ out[`y${i}_${k}`] = RACE.top + (rank.get(i) ?? i) * RACE.pitch;
27702
+ out[`w${i}_${k}`] = w;
27703
+ out[`x${i}_${k}`] = r114(RACE.left + w + 5);
27704
+ out[`v${i}_${k}`] = value;
27705
+ }
27706
+ }
27707
+ for (let k = 1; k < RACE_STATES; k++) {
27708
+ out[`t${k}`] = k < states ? raceTime(k) : end;
27709
+ out[`m${k}`] = k < states ? RACE.move : 0;
27710
+ out[`tick${k}`] = k < states ? raceTime(k) + RACE.move / 2 : end;
27711
+ out[`solo${k}`] = Math.min(k, states - 1);
27712
+ }
27713
+ for (let k = 0; k < RACE_STATES; k++) {
27714
+ const p = fit7(periods[k] ?? "", 120, 30);
27715
+ out[`p${k}`] = periods[k] ?? "";
27716
+ out[`ps${k}`] = p.size;
27717
+ }
27718
+ return out;
27719
+ },
27720
+ motion: {
27721
+ svg: `<svg viewBox="0 0 ${RACE.width} 210" font-family="${SANS12}"><!-- {{rows: text default='Lumen:12,28,41,66;Pixel:30,36,48,54;Vertex:22,40,39,71;Shader:8,20,52,60;Raster:18,24,30,35;Vector:26,30,44,49' label='Rows (Name:v1,v2,\u2026; \u2026)'}} {{periods: text default='2021,2022,2023,2024' label='Periods (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} ${palette()} --><g id="barRacePeriod" font-weight="800" fill="{{ink}}" fill-opacity="0.22" text-anchor="end">${Array.from(
27722
+ { length: RACE_STATES },
27723
+ (_, k) => `<text x="${RACE.width - 8}" y="202" font-size="{{ps${k}}}"${k ? ` display="none"` : ""}>{{p${k}}}</text>`
27724
+ ).join("")}</g>${Array.from(
27725
+ { length: RACE_ROWS },
27726
+ (_, i) => `<g id="barRaceRow${i}" display="{{show${i}}}" opacity="0" transform="translate(0 {{y${i}_0}})"><text x="${RACE.left - 6}" y="15" text-anchor="end" font-size="{{ns${i}}}" textLength="{{nl${i}}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}">{{nm${i}}}</text><rect id="barRaceBar${i}" x="${RACE.left}" y="0" width="0" height="${RACE.height}" rx="4" fill="{{c${i + 1}}}"/><text id="barRaceValue${i}" x="${RACE.left + 5}" y="15" font-size="${RACE.value}" font-weight="700" fill="{{ink}}">0</text></g>`
27727
+ ).join("")}</svg>`,
27728
+ animate: [
27729
+ { id: "barRacePeriod", solo: 0 },
27730
+ ...Array.from({ length: RACE_STATES - 1 }, (_, j) => ({ id: "barRacePeriod", solo: n15(`solo${j + 1}`), delay: n15(`tick${j + 1}`) })),
27731
+ ...Array.from({ length: RACE_ROWS }, (_, i) => [
27732
+ { id: `barRaceRow${i}`, attrs: { opacity: [0, 1] }, delay: n15(`in${i}`), ms: 300 },
27733
+ { id: `barRaceBar${i}`, attrs: { width: [0, n15(`w${i}_0`)] }, delay: n15(`in${i}`), ms: 800, ease: "out" },
27734
+ { id: `barRaceValue${i}`, attrs: { x: [RACE.left + 5, n15(`x${i}_0`)] }, count: [0, n15(`v${i}_0`)], decimals: n15("dec"), delay: n15(`in${i}`), ms: 800, ease: "out" },
27735
+ ...Array.from({ length: RACE_STATES - 1 }, (_2, j) => {
27736
+ const [a, b] = [j, j + 1];
27737
+ const at4 = { delay: n15(`t${b}`), ms: n15(`m${b}`), ease: "in-out" };
27738
+ return [
27739
+ { id: `barRaceRow${i}`, attrs: { transform: [`translate(0 {{y${i}_${a}}})`, `translate(0 {{y${i}_${b}}})`] }, ...at4 },
27740
+ { id: `barRaceBar${i}`, attrs: { width: [n15(`w${i}_${a}`), n15(`w${i}_${b}`)] }, ...at4 },
27741
+ { id: `barRaceValue${i}`, attrs: { x: [n15(`x${i}_${a}`), n15(`x${i}_${b}`)] }, count: [n15(`v${i}_${a}`), n15(`v${i}_${b}`)], decimals: n15("dec"), ...at4 }
27742
+ ];
27743
+ }).flat()
27744
+ ]).flat()
27745
+ ]
27746
+ }
27747
+ };
27748
+ var SPARK_DEFAULT = [4, 6, 5, 8, 7, 10, 9, 13];
27749
+ var SPARK_DRAW = 1200;
27750
+ function linePath(values, x0, x1, top, bottom) {
27751
+ const lo = Math.min(...values);
27752
+ const hi = Math.max(...values);
27753
+ const span = hi - lo || 1;
27754
+ const pts = values.map((v, j) => [r114(x0 + j * (x1 - x0) / (values.length - 1)), r114(hi === lo ? (top + bottom) / 2 : bottom - (v - lo) / span * (bottom - top))]);
27755
+ const line = pts.map(([x, y], j) => `${j ? "L" : "M"}${x} ${y}`).join(" ");
27756
+ return { line, area: `${line} L${pts.at(-1)[0]} ${bottom + 3} L${pts[0][0]} ${bottom + 3}Z`, end: pts.at(-1) };
27757
+ }
27758
+ function series(text, max, fallback) {
27759
+ const values = parseNumberList(text, max).filter((x) => Number.isFinite(x));
27760
+ return values.length >= 2 ? values : values.length === 1 ? [values[0], values[0]] : fallback;
27761
+ }
27762
+ var sparkline = {
27763
+ kind: "vector",
27764
+ id: "sparkline",
27765
+ name: "Sparkline",
27766
+ description: "A tiny trend line drawing in with a dot riding its tip, which then pulses beside the latest value; small enough to sit in a line of text.",
27767
+ category: "data",
27768
+ tags: ["chart", "sparkline", "trend", "inline", "kpi", "dashboard"],
27769
+ hold: 600,
27770
+ presets: [
27771
+ { name: "Falling", values: { values: "14,13,11,12,9,8,9,6", accent: [0.86, 0.15, 0.15] } },
27772
+ { name: "Violet", values: { accent: [0.55, 0.36, 0.96] } }
27773
+ ],
27774
+ derived: (v) => {
27775
+ const values = series(v.values, 32, SPARK_DEFAULT);
27776
+ const dec = decimalsOf(values);
27777
+ const e = Math.max(0.6, textWidthEm(formatCount(values.at(-1), { decimals: dec })));
27778
+ const size = r114(Math.min(13, 44 / e));
27779
+ const right = r114(120 - 3 - e * size - 6);
27780
+ const { line, area } = linePath(values, 4, right, 6, 26);
27781
+ return { line, area, size, valueX: r114(right + 6), first: values[0], last: values.at(-1), dec };
27782
+ },
27783
+ motion: {
27784
+ svg: `<svg viewBox="0 0 120 32" font-family="${SANS12}"><!-- {{values: text default='4,6,5,8,7,10,9,13' label='Values (comma-separated)'}} {{accent: color default=#10B981 label='Line'}} {{ink: color default=#1E293B label='Value'}} --><defs><linearGradient id="sparklineFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.3"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><path id="sparklineArea" d="{{area}}" fill="url(#sparklineFill)" opacity="0"/><path id="sparklineLine" d="{{line}}" fill="none" stroke="{{accent}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g id="sparklineDot"><circle id="sparklinePulse" r="2.6" fill="none" stroke="{{accent}}" stroke-width="1.5" opacity="0"/><circle r="2.8" fill="{{accent}}"/></g><text id="sparklineValue" x="{{valueX}}" y="20.5" font-size="{{size}}" font-weight="700" fill="{{ink}}">0</text></svg>`,
27785
+ animate: [
27786
+ { id: "sparklineLine", draw: true, ms: SPARK_DRAW, ease: "in-out" },
27787
+ { id: "sparklineDot", along: "sparklineLine", ms: SPARK_DRAW, ease: "in-out" },
27788
+ { id: "sparklineArea", attrs: { opacity: [0, 1] }, delay: 700, ms: 600 },
27789
+ { id: "sparklineValue", count: [n15("first"), n15("last")], decimals: n15("dec"), ms: SPARK_DRAW, ease: "in-out" },
27790
+ ...[0, 1, 2].map((k) => ({ id: "sparklinePulse", attrs: { r: [2.6, 9], opacity: [0.8, 0] }, delay: SPARK_DRAW + 50 + k * 750, ms: 700, ease: "out" }))
27791
+ ]
27792
+ }
27793
+ };
27794
+ var BOARD = { rows: 6, top: 8, pitch: 34, height: 30, left: 8, width: 244, slide: 130 };
27795
+ var leaderboard = {
27796
+ kind: "vector",
27797
+ id: "leaderboard",
27798
+ name: "Leaderboard",
27799
+ description: "Ranked rows sliding into place from the bottom up, each with an initials avatar and a score counting up, then a highlight on the winner.",
27800
+ category: "data",
27801
+ tags: ["leaderboard", "ranking", "scores", "game", "dashboard", "competition"],
27802
+ hold: 1800,
27803
+ presets: [
27804
+ { name: "Dark", values: { card: [0.12, 0.14, 0.19], ink: [0.94, 0.95, 0.97] } },
27805
+ { name: "Sales", values: { names: "North team,South team,East team,West team", scores: "128400,119250,98100,87600", accent: [0.13, 0.77, 0.37] } }
27806
+ ],
27807
+ derived: (v) => {
27808
+ const names = parseLabels(v.names, BOARD.rows);
27809
+ const scores = parseNumberList(v.scores, BOARD.rows).map((x) => Number.isFinite(x) ? x : 0);
27810
+ const count = Math.min(BOARD.rows, Math.max(1, names.filter((s) => s !== "").length, scores.length));
27811
+ const rows = Array.from({ length: count }, (_, i) => ({ name: names[i] || `Player ${i + 1}`, score: scores[i] ?? 0 }));
27812
+ const order = rows.map((r, i) => [i, r.score]).sort((a, b) => b[1] - a[1] || a[0] - b[0]);
27813
+ const rank = new Map(order.map(([i], place) => [i, place]));
27814
+ const dec = decimalsOf(rows.map((r) => r.score));
27815
+ const crown = (count - 1) * BOARD.slide + 450;
27816
+ const out = { dec, crown, settle: crown + 200 };
27817
+ for (let i = 0; i < BOARD.rows; i++) {
27818
+ const row = rows[i];
27819
+ const place = rank.get(i) ?? i;
27820
+ const wait = Math.max(0, count - 1 - place) * BOARD.slide;
27821
+ const scoreWidth = textWidthEm(formatCount(row?.score ?? 0, { decimals: dec })) * 12;
27822
+ const name = fitLabel(row?.name ?? "", Math.max(20, BOARD.left + BOARD.width - 12 - scoreWidth - 8 - 72), 12, 9);
27823
+ const initials = (row?.name ?? "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => Array.from(w)[0]).join("").toUpperCase() || "?";
27824
+ out[`show${i}`] = row ? "inline" : "none";
27825
+ out[`y${i}`] = BOARD.top + place * BOARD.pitch;
27826
+ out[`at${i}`] = wait;
27827
+ out[`land${i}`] = wait + 300;
27828
+ out[`rank${i}`] = place + 1;
27829
+ out[`nm${i}`] = name.text;
27830
+ out[`ns${i}`] = name.size;
27831
+ out[`nl${i}`] = name.length;
27832
+ out[`ini${i}`] = initials;
27833
+ out[`is${i}`] = r114(Math.min(10, 15 / Math.max(0.6, textWidthEm(initials))));
27834
+ out[`sc${i}`] = row?.score ?? 0;
27835
+ }
27836
+ return out;
27837
+ },
27838
+ motion: {
27839
+ svg: `<svg viewBox="0 0 260 214" font-family="${SANS12}"><!-- {{names: text default='Ava Stone,Ben Ito,Cleo Park,Dev Shah,Eli Moss' label='Names (comma-separated)'}} {{scores: text default='870,980,640,820,510' label='Scores (comma-separated)'}} {{card: color default=#F1F5F9 label='Rows'}} {{ink: color default=#0F172A label='Text'}} {{accent: color default=#F59E0B label='Winner highlight'}} ${palette()} -->${Array.from(
27840
+ { length: BOARD.rows },
27841
+ (_, i) => `<g id="leaderboardRow${i}" display="{{show${i}}}" opacity="0" transform="translate(280 {{y${i}}})"><rect x="${BOARD.left}" y="0" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="{{card}}"/><text x="26" y="19.5" text-anchor="middle" font-size="12" font-weight="700" fill="{{ink}}" fill-opacity="0.55">{{rank${i}}}</text><circle cx="52" cy="15" r="11" fill="{{c${i + 1}}}"/><text x="52" y="18.5" text-anchor="middle" font-size="{{is${i}}}" font-weight="700" fill="#FFFFFF">{{ini${i}}}</text><text x="70" y="19.5" font-size="{{ns${i}}}" textLength="{{nl${i}}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}">{{nm${i}}}</text><text id="leaderboardScore${i}" x="${BOARD.left + BOARD.width - 12}" y="19.5" text-anchor="end" font-size="12" font-weight="700" fill="{{ink}}">0</text></g>`
27842
+ ).join(
27843
+ ""
27844
+ )}<rect id="leaderboardRing" x="${BOARD.left}" y="${BOARD.top}" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="none" stroke="{{accent}}" stroke-width="2" opacity="0"/><rect id="leaderboardTop" x="${BOARD.left}" y="${BOARD.top}" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="{{accent}}" fill-opacity="0.12" stroke="{{accent}}" stroke-width="2" opacity="0"/><g id="leaderboardBadge" transform="translate(26 23) scale(0)"><circle r="10" fill="{{accent}}"/><path d="M-5 -3.5 L-2.5 -0.5 L0 -5 L2.5 -0.5 L5 -3.5 L4 3.5 H-4Z" fill="#FFFFFF"/></g></svg>`,
27845
+ animate: [
27846
+ ...Array.from({ length: BOARD.rows }, (_, i) => [
27847
+ { id: `leaderboardRow${i}`, attrs: { transform: [`translate(280 {{y${i}}})`, `translate(0 {{y${i}}})`] }, delay: n15(`at${i}`), ms: 450, ease: "out" },
27848
+ { id: `leaderboardRow${i}`, attrs: { opacity: [0, 1] }, delay: n15(`at${i}`), ms: 200 },
27849
+ { id: `leaderboardScore${i}`, count: [0, n15(`sc${i}`)], decimals: n15("dec"), delay: n15(`land${i}`), ms: 800, ease: "out" }
27850
+ ]).flat(),
27851
+ { id: "leaderboardTop", attrs: { opacity: [0, 1] }, delay: n15("crown"), ms: 250 },
27852
+ { id: "leaderboardRing", attrs: { opacity: [0, 0.9] }, delay: n15("crown"), ms: 0 },
27853
+ { id: "leaderboardRing", attrs: { x: [BOARD.left, BOARD.left - 5], y: [BOARD.top, BOARD.top - 5], width: [BOARD.width, BOARD.width + 10], height: [BOARD.height, BOARD.height + 10], opacity: [0.9, 0] }, delay: n15("crown"), ms: 650, ease: "out" },
27854
+ { id: "leaderboardBadge", attrs: { transform: ["translate(26 23) scale(0)", "translate(26 23) scale(1.25)"] }, delay: n15("crown"), ms: 200, ease: "out" },
27855
+ { id: "leaderboardBadge", attrs: { transform: ["translate(26 23) scale(1.25)", "translate(26 23) scale(1)"] }, delay: n15("settle"), ms: 200, ease: "in-out" }
27856
+ ]
27857
+ }
27858
+ };
27859
+ var AREA = { series: 4, points: 12, x0: 34, x1: 268, top: 40, base: 150, sweep: 2400, rise: 1100 };
27860
+ var AREA_DEFAULT = [
27861
+ [12, 18, 15, 24, 28, 34],
27862
+ [8, 10, 14, 12, 18, 22],
27863
+ [5, 6, 9, 8, 11, 14]
27864
+ ];
27865
+ var areaChart = {
27866
+ kind: "vector",
27867
+ id: "area-chart",
27868
+ name: "Area chart",
27869
+ description: "A stacked area chart whose layers rise together from the baseline, then a tooltip line glides across reading the total.",
27870
+ category: "data",
27871
+ tags: ["chart", "area", "stacked", "trend", "dashboard", "report"],
27872
+ hold: 1400,
27873
+ presets: [...PALETTE_PRESETS.map((p) => ({ ...p, values: Object.fromEntries(Object.entries(p.values).slice(0, AREA.series)) })), { name: "Two series", values: { series: "5,9,7,12,15,14,18,22;3,4,6,5,8,9,8,11", labels: "Paid,Organic", points: "W1,W2,W3,W4,W5,W6,W7,W8" } }],
27874
+ derived: (v) => {
27875
+ const rows = parseSeries(v.series, AREA.series, AREA.points, AREA_DEFAULT);
27876
+ const points = rows[0].length;
27877
+ const cum = rows.map((_, s) => Array.from({ length: points }, (_2, j) => rows.slice(0, s + 1).reduce((a, r) => a + r[j], 0)));
27878
+ const top = Math.max(1e-9, ...cum.at(-1));
27879
+ const x = (j) => r114(AREA.x0 + j * (AREA.x1 - AREA.x0) / (points - 1));
27880
+ const y = (value) => r114(AREA.base - value / top * (AREA.base - AREA.top));
27881
+ const edge = (values, back) => {
27882
+ const pts = values.map((val, j) => `${x(j)} ${y(val)}`);
27883
+ return (back ? pts.reverse() : pts).join(" L");
27884
+ };
27885
+ const zero = new Array(points).fill(0);
27886
+ const labels = parseLabels(v.labels, AREA.series);
27887
+ const xLabels = parseLabels(v.points, AREA.points);
27888
+ const dec = decimalsOf(rows.flat());
27889
+ const totals = cum.at(-1);
27890
+ const spacing = (AREA.x1 - AREA.x0) / (points - 1);
27891
+ const seg = AREA.sweep / (points - 1);
27892
+ const tip = AREA.rise + 200;
27893
+ const end = tip + AREA.sweep;
27894
+ const tipSize = r114(Math.min(9, 34 / Math.max(0.6, ...totals.map((t) => textWidthEm(formatCount(t, { decimals: dec }))))));
27895
+ const out = { dec, tip, tipSize, tx0: x(0), total0: +totals[0].toFixed(dec) };
27896
+ for (let s = 0; s < AREA.series; s++) {
27897
+ const shown = s < rows.length;
27898
+ const below = s > 0 && shown ? cum[s - 1] : zero;
27899
+ const above = shown ? cum[s] : zero;
27900
+ out[`show${s}`] = shown ? "inline" : "none";
27901
+ out[`flat${s}`] = `M${edge(zero, false)} L${edge(zero, true)}Z`;
27902
+ out[`full${s}`] = `M${edge(above, false)} L${edge(below, true)}Z`;
27903
+ const lab = fitLabel(labels[s] ?? "", 50, 9, 7.5);
27904
+ out[`lab${s}`] = lab.text;
27905
+ out[`labs${s}`] = lab.size;
27906
+ out[`labl${s}`] = lab.length;
27907
+ out[`dy${s}_0`] = y(above[0]);
27908
+ }
27909
+ for (let j = 0; j < AREA.points; j++) {
27910
+ const shown = j < points;
27911
+ const label = fitLabel(xLabels[j] ?? "", spacing * 0.9, 9, 7);
27912
+ out[`xs${j}`] = shown ? "inline" : "none";
27913
+ out[`xx${j}`] = shown ? x(j) : AREA.x1;
27914
+ out[`xl${j}`] = label.text;
27915
+ out[`xz${j}`] = label.size;
27916
+ }
27917
+ for (let j = 0; j < AREA.points - 1; j++) {
27918
+ const real = j < points - 1;
27919
+ const [a, b] = real ? [j, j + 1] : [points - 1, points - 1];
27920
+ out[`sd${j}`] = real ? Math.round(tip + j * seg) : end;
27921
+ out[`sm${j}`] = real ? Math.round(seg) : 0;
27922
+ out[`ta${j}`] = x(a);
27923
+ out[`tb${j}`] = x(b);
27924
+ out[`va${j}`] = +totals[a].toFixed(dec);
27925
+ out[`vb${j}`] = +totals[b].toFixed(dec);
27926
+ for (let s = 0; s < AREA.series; s++) {
27927
+ const c = s < rows.length ? cum[s] : zero;
27928
+ out[`ya${s}_${j}`] = y(c[a]);
27929
+ out[`yb${s}_${j}`] = y(c[b]);
27930
+ }
27931
+ }
27932
+ return out;
27933
+ },
27934
+ motion: {
27935
+ svg: `<svg viewBox="0 0 300 176" font-family="${SANS12}"><!-- {{series: text default='12,18,15,24,28,34;8,10,14,12,18,22;5,6,9,8,11,14' label='Series (values, rows split by ;)'}} {{labels: text default='Web,Mobile,API' label='Series labels (comma-separated)'}} {{points: text default='Jan,Feb,Mar,Apr,May,Jun' label='Point labels (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} {{grid: color default=#E2E8F0 label='Grid'}} ${palette(AREA.series)} --><g stroke="{{grid}}" stroke-width="1">${[0, 1, 2, 3].map((k) => `<line x1="${AREA.x0}" x2="${AREA.x1}" y1="${AREA.top + k * (AREA.base - AREA.top) / 3}" y2="${AREA.top + k * (AREA.base - AREA.top) / 3}"/>`).join("")}</g>${Array.from(
27936
+ { length: AREA.series },
27937
+ (_, s) => `<path id="areaChartArea${s}" display="{{show${s}}}" d="{{flat${s}}}" fill="{{c${s + 1}}}" fill-opacity="0.78" stroke="{{c${s + 1}}}" stroke-width="1.5" stroke-linejoin="round"/>`
27938
+ ).join("")}${Array.from(
27939
+ { length: AREA.series },
27940
+ (_, s) => `<g display="{{show${s}}}" transform="translate(${AREA.x0 + s * 64} 0)"><rect x="0" y="5" width="8" height="8" rx="2" fill="{{c${s + 1}}}"/><text x="12" y="12.5" font-size="{{labs${s}}}" textLength="{{labl${s}}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}">{{lab${s}}}</text></g>`
27941
+ ).join("")}<g font-size="9" fill="{{ink}}" fill-opacity="0.6" text-anchor="middle">${Array.from(
27942
+ { length: AREA.points },
27943
+ (_, j) => `<text display="{{xs${j}}}" x="{{xx${j}}}" y="166" font-size="{{xz${j}}}">{{xl${j}}}</text>`
27944
+ ).join("")}</g><g id="areaChartTip" opacity="0" transform="translate({{tx0}} 0)"><line x1="0" x2="0" y1="34" y2="${AREA.base}" stroke="{{ink}}" stroke-opacity="0.5" stroke-width="1.2" stroke-dasharray="3 2"/><rect x="-20" y="18" width="40" height="16" rx="4" fill="{{ink}}"/><text id="areaChartTipValue" x="0" y="29" text-anchor="middle" font-size="{{tipSize}}" font-weight="700" fill="#FFFFFF">0</text>${Array.from(
27945
+ { length: AREA.series },
27946
+ (_, s) => `<circle id="areaChartDot${s}" display="{{show${s}}}" cx="0" cy="{{dy${s}_0}}" r="3" fill="#FFFFFF" stroke="{{c${s + 1}}}" stroke-width="2"/>`
27947
+ ).join("")}</g></svg>`,
27948
+ animate: [
27949
+ ...Array.from({ length: AREA.series }, (_, s) => ({ id: `areaChartArea${s}`, attrs: { d: [`{{flat${s}}}`, `{{full${s}}}`] }, ms: AREA.rise, ease: "out" })),
27950
+ { id: "areaChartTip", attrs: { opacity: [0, 1] }, delay: n15("tip"), ms: 200 },
27951
+ { id: "areaChartTipValue", count: [n15("total0"), n15("total0")], decimals: n15("dec"), delay: n15("tip"), ms: 0 },
27952
+ ...Array.from({ length: AREA.points - 1 }, (_, j) => {
27953
+ const at4 = { delay: n15(`sd${j}`), ms: n15(`sm${j}`), ease: "linear" };
27954
+ return [
27955
+ { id: "areaChartTip", attrs: { transform: [`translate({{ta${j}}} 0)`, `translate({{tb${j}}} 0)`] }, ...at4 },
27956
+ { id: "areaChartTipValue", count: [n15(`va${j}`), n15(`vb${j}`)], decimals: n15("dec"), ...at4 },
27957
+ ...Array.from({ length: AREA.series }, (_2, s) => ({ id: `areaChartDot${s}`, attrs: { cy: [n15(`ya${s}_${j}`), n15(`yb${s}_${j}`)] }, ...at4 }))
27958
+ ];
27959
+ }).flat()
27960
+ ]
27961
+ }
27962
+ };
27963
+ var KPI_DEFAULT = [3, 4, 3.5, 5, 4.8, 6, 7.2];
27964
+ var kpiTile = {
27965
+ kind: "vector",
27966
+ id: "kpi-tile",
27967
+ name: "KPI tile",
27968
+ description: "A dashboard tile: the headline figure counts up, a delta arrow shows the change in green or red by its sign, and a mini sparkline draws in.",
27969
+ category: "data",
27970
+ tags: ["kpi", "dashboard", "metric", "card", "sparkline", "report"],
27971
+ hold: 1600,
27972
+ presets: [
27973
+ { name: "Dark", values: { card: [0.09, 0.11, 0.15], ink: [0.95, 0.96, 0.97] } },
27974
+ { name: "Churn", values: { label: "Churn rate", value: 2.4, prefix: "", suffix: "%", delta: -0.6, trend: "3.4,3.3,3.1,3.0,2.8,2.6,2.4", caption: "vs last quarter" } }
27975
+ ],
27976
+ derived: (v) => {
27977
+ const value = Number(v.value) || 0;
27978
+ const dec = Math.min(3, Math.max(0, Math.round(Number(v.decimals) || 0)));
27979
+ const pre = formatSafe(v.prefix);
27980
+ const suf = formatSafe(v.suffix);
27981
+ const big = fit7(`${pre}${formatCount(value, { decimals: dec })}${suf}`, 204, 34);
27982
+ const delta = Number(v.delta) || 0;
27983
+ const up = delta >= 0;
27984
+ const trend = series(v.trend, 24, KPI_DEFAULT);
27985
+ const { line, area, end } = linePath(trend, 148, 222, 90, 120);
27986
+ const label = fitLabel(v.label, 204, 12, 10);
27987
+ const caption = fitLabel(v.caption, 120, 10, 8);
27988
+ return {
27989
+ pre,
27990
+ suf,
27991
+ dec,
27992
+ value: +value.toFixed(dec),
27993
+ size: big.size,
27994
+ delta: +Math.abs(delta).toFixed(1),
27995
+ deltaFormat: up ? "+{n}%" : "\u2212{n}%",
27996
+ deltaColor: paint(up ? v.up : v.down),
27997
+ arrow: up ? "M24 92 L30 100 H18Z" : "M24 101 L30 93 H18Z",
27998
+ line,
27999
+ area,
28000
+ endX: end[0],
28001
+ endY: end[1],
28002
+ labelText: label.text,
28003
+ captionText: caption.text,
28004
+ labelSize: label.size,
28005
+ labelLength: label.length,
28006
+ captionSize: caption.size,
28007
+ captionLength: caption.length
28008
+ };
28009
+ },
28010
+ motion: {
28011
+ svg: `<svg viewBox="0 0 240 140" font-family="${SANS12}"><!-- {{label: text default='Monthly revenue' label='Label'}} {{value: float min=0 max=1000000 default=48.2 step=0.1 label='Value'}} {{decimals: int min=0 max=3 default=1 label='Decimals'}} {{prefix: text default='$' label='Prefix'}} {{suffix: text default='k' label='Suffix'}} {{delta: float min=-100 max=100 default=12.4 step=0.1 label='Change (%)'}} {{caption: text default='vs last month' label='Caption'}} {{trend: text default='3,4,3.5,5,4.8,6,7.2' label='Trend (comma-separated)'}} {{card: color default=#FFFFFF label='Card'}} {{ink: color default=#0F172A label='Text'}} {{accent: color default=#6366F1 label='Sparkline'}} {{up: color default=#16A34A label='Rise'}} {{down: color default=#DC2626 label='Fall'}} --><defs><linearGradient id="kpiTileFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.28"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><rect x="2" y="2" width="236" height="136" rx="14" fill="{{card}}" stroke="{{ink}}" stroke-opacity="0.1"/><text x="18" y="30" font-size="{{labelSize}}" textLength="{{labelLength}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}" fill-opacity="0.6">{{labelText}}</text><text id="kpiTileValue" x="18" y="72" font-size="{{size}}" font-weight="800" fill="{{ink}}">0</text><g id="kpiTileDelta" opacity="0"><path d="{{arrow}}" fill="{{deltaColor}}"/><text id="kpiTileDeltaValue" x="34" y="101" font-size="12" font-weight="700" fill="{{deltaColor}}">0%</text></g><text id="kpiTileCaption" x="18" y="124" font-size="{{captionSize}}" textLength="{{captionLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.55" opacity="0">{{captionText}}</text><path id="kpiTileArea" d="{{area}}" fill="url(#kpiTileFill)" opacity="0"/><path id="kpiTileLine" d="{{line}}" fill="none" stroke="{{accent}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle id="kpiTileEnd" cx="{{endX}}" cy="{{endY}}" r="0" fill="{{accent}}"/></svg>`,
28012
+ animate: [
28013
+ { id: "kpiTileValue", count: [0, n15("value")], decimals: n15("dec"), format: "{{pre}}{n}{{suf}}", ms: 1400, ease: "out" },
28014
+ { id: "kpiTileDelta", attrs: { opacity: [0, 1], transform: ["translate(0 6)", "translate(0 0)"] }, delay: 500, ms: 400, ease: "out" },
28015
+ { id: "kpiTileDeltaValue", count: [0, n15("delta")], decimals: 1, format: "{{deltaFormat}}", delay: 500, ms: 900, ease: "out" },
28016
+ { id: "kpiTileCaption", attrs: { opacity: [0, 1] }, delay: 800, ms: 400 },
28017
+ { id: "kpiTileLine", draw: true, delay: 300, ms: 1100, ease: "in-out" },
28018
+ { id: "kpiTileArea", attrs: { opacity: [0, 1] }, delay: 900, ms: 500 },
28019
+ { id: "kpiTileEnd", attrs: { r: [0, 4.5] }, delay: 1400, ms: 180, ease: "out" },
28020
+ { id: "kpiTileEnd", attrs: { r: [4.5, 3] }, delay: 1580, ms: 200, ease: "in-out" }
28021
+ ]
28022
+ }
28023
+ };
28024
+ var dataEffects = [donutChart, pieChart, gauge, barRace, sparkline, leaderboard, areaChart, kpiTile];
28025
+
28026
+ // src/catalog/icons.ts
28027
+ var r115 = (v) => Math.round(v * 10) / 10;
28028
+ var r213 = (v) => Math.round(v * 100) / 100;
28029
+ var n16 = (name) => `{{${name}}}`;
28030
+ var SANS13 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
28031
+ var about6 = (cx, cy, s) => `translate(${cx} ${cy}) scale(${r213(s)}) translate(${-cx} ${-cy})`;
28032
+ function cycle(id, from, to, phase, period, ease = "linear") {
28033
+ const at4 = (u) => Object.fromEntries(Object.keys(from).map((k) => [k, lerpAttr(from[k], to[k], u)]));
28034
+ const mid = at4(phase);
28035
+ const pairs = (a, b) => Object.fromEntries(Object.keys(from).map((k) => [k, [a[k], b[k]]]));
28036
+ const first = Math.round((1 - phase) * period);
28037
+ const out = [{ id, attrs: pairs(mid, to), ms: first, ease }];
28038
+ if (phase > 0) out.push({ id, attrs: pairs(from, mid), delay: first, ms: period - first, ease });
28039
+ return out;
28040
+ }
28041
+ var swing = (id, attr, a, b, period, delay = 0) => [
28042
+ { id, attrs: { [attr]: [a, b] }, delay, ms: period / 2, ease: "in-out" },
28043
+ { id, attrs: { [attr]: [b, a] }, delay: delay + period / 2, ms: period / 2, ease: "in-out" }
28044
+ ];
28045
+ var picks = (name, count) => (v) => {
28046
+ const pick = Math.min(count - 1, Math.max(0, Math.round(Number(v[name]) || 0)));
28047
+ return Object.fromEntries(Array.from({ length: count }, (_, k) => [`show${k}`, k === pick ? "inline" : "none"]));
28048
+ };
28049
+ var WEATHER_MS = 2400;
28050
+ var CLOUD = "M-22 14 H22 A12 12 0 0 0 20 -9 A17 17 0 0 0 -12 -10 A12 12 0 0 0 -22 14Z";
28051
+ var DROP = "M0 -5 C2 -1 3 1 3 3 A3 3 0 0 1 -3 3 C-3 1 -2 -1 0 -5Z";
28052
+ var FLAKE = "M0 -4 V4 M-3.5 -2 L3.5 2 M-3.5 2 L3.5 -2";
28053
+ var sunRays = (cx, cy, from, to) => Array.from({ length: 8 }, (_, k) => {
28054
+ const a = k * Math.PI / 4;
28055
+ return `M${r115(cx + from * Math.cos(a))} ${r115(cy + from * Math.sin(a))} L${r115(cx + to * Math.cos(a))} ${r115(cy + to * Math.sin(a))}`;
28056
+ }).join(" ");
28057
+ var cloud2 = (id, x, y, s, fill, extra = "") => `<g id="${id}" transform="translate(${x} ${y}) scale(${s})"><path d="${CLOUD}" fill="${fill}"${extra}/></g>`;
28058
+ var falling = (prefix, shape, xs, top, bottom, spin = 0) => ({
28059
+ markup: xs.map((x, i) => `<g id="${prefix}${i}" transform="translate(${x} ${top}) rotate(0)" opacity="1">${shape(i)}</g>`).join(""),
28060
+ animate: xs.flatMap(
28061
+ (x, i) => cycle(`${prefix}${i}`, { transform: `translate(${x} ${top}) rotate(0)`, opacity: 1 }, { transform: `translate(${x + spin / 30} ${bottom}) rotate(${spin})`, opacity: 0 }, (i * 0.37 + 0.15) % 1, WEATHER_MS)
28062
+ )
28063
+ });
28064
+ var rain = falling("weatherIconsDrop", () => `<path d="${DROP}" fill="{{accent}}"/>`, [36, 50, 64, 43, 57], 50, 86);
28065
+ var storm = falling("weatherIconsStormDrop", () => `<path d="${DROP}" fill="{{accent}}"/>`, [34, 66], 50, 86);
28066
+ var snow = falling("weatherIconsFlake", () => `<path d="${FLAKE}" stroke="{{accent}}" stroke-width="1.8" stroke-linecap="round"/>`, [36, 50, 64, 43, 58], 50, 88, 120);
28067
+ var WIND = ["M14 38 H58 C68 38 70 26 62 24 C56 23 54 29 58 32", "M10 52 H76 C86 52 88 64 80 66 C74 67 72 61 76 58", "M20 66 H50"];
28068
+ var weatherIcons = {
28069
+ kind: "vector",
28070
+ id: "weather-icons",
28071
+ name: "Weather icons",
28072
+ description: "An animated weather icon: sun with turning rays, partly cloudy, drifting clouds, falling rain, a flashing thunderbolt, snow or gusting wind.",
28073
+ category: "icon",
28074
+ tags: ["weather", "forecast", "sun", "rain", "snow", "loop"],
28075
+ hold: 0,
28076
+ presets: [
28077
+ { name: "Rain", values: { kind: 3 } },
28078
+ { name: "Snow", values: { kind: 5, cloud: [0.8, 0.85, 0.92], accent: [0.55, 0.75, 0.95] } },
28079
+ { name: "Thunder", values: { kind: 4, cloud: [0.39, 0.45, 0.55] } }
28080
+ ],
28081
+ derived: picks("kind", 7),
28082
+ motion: {
28083
+ svg: `<svg viewBox="0 0 100 100"><!-- {{kind: int min=0 max=6 default=0 label='Weather (sun, partly cloudy, cloudy, rain, thunder, snow, wind)'}} {{sun: color default=#FBBF24 label='Sun'}} {{cloud: color default=#94A3B8 label='Cloud'}} {{accent: color default=#38BDF8 label='Rain, snow and wind'}} {{bolt: color default=#FACC15 label='Lightning'}} --><g display="{{show0}}"><g id="weatherIconsRays" transform="rotate(0 50 50)"><path d="${sunRays(50, 50, 26, 36)}" stroke="{{sun}}" stroke-width="5" stroke-linecap="round"/></g><circle id="weatherIconsSun" cx="50" cy="50" r="17" fill="{{sun}}"/></g><g display="{{show1}}"><g id="weatherIconsPartRays" transform="rotate(0 38 38)"><path d="${sunRays(38, 38, 19, 27)}" stroke="{{sun}}" stroke-width="4" stroke-linecap="round"/></g><circle cx="38" cy="38" r="13" fill="{{sun}}"/>${cloud2("weatherIconsPartCloud", 56, 60, 1, "{{cloud}}")}</g><g display="{{show2}}">${cloud2("weatherIconsBackCloud", 62, 40, 0.75, "{{cloud}}", ` opacity="0.55"`)}${cloud2("weatherIconsFrontCloud", 46, 56, 1.05, "{{cloud}}")}</g><g display="{{show3}}">${rain.markup}${cloud2("weatherIconsRainCloud", 50, 38, 1.1, "{{cloud}}")}</g><g display="{{show4}}">${storm.markup}<g transform="translate(50 36) scale(1.1)"><path d="${CLOUD}" fill="{{cloud}}"/><path d="${CLOUD}" fill="#000000" opacity="0.22"/></g><path id="weatherIconsBolt" d="M54 50 L42 70 H51 L45 90 L63 64 H54 L60 50Z" fill="{{bolt}}" opacity="0"/></g><g display="{{show5}}">${snow.markup}${cloud2("weatherIconsSnowCloud", 50, 38, 1.1, "{{cloud}}")}</g><g display="{{show6}}" fill="none" stroke="{{accent}}" stroke-width="5" stroke-linecap="round">${WIND.map((d, k) => `<path id="weatherIconsWind${k}" d="${d}"/>`).join("")}</g></svg>`,
28084
+ animate: [
28085
+ ...cycle("weatherIconsRays", { transform: "rotate(0 50 50)" }, { transform: "rotate(45 50 50)" }, 0, WEATHER_MS),
28086
+ ...swing("weatherIconsSun", "r", 17, 18.5, WEATHER_MS),
28087
+ ...cycle("weatherIconsPartRays", { transform: "rotate(0 38 38)" }, { transform: "rotate(45 38 38)" }, 0, WEATHER_MS),
28088
+ ...swing("weatherIconsPartCloud", "transform", "translate(54 60) scale(1)", "translate(60 60) scale(1)", WEATHER_MS),
28089
+ ...swing("weatherIconsBackCloud", "transform", "translate(66 40) scale(0.75)", "translate(58 40) scale(0.75)", WEATHER_MS),
28090
+ ...swing("weatherIconsFrontCloud", "transform", "translate(43 56) scale(1.05)", "translate(50 56) scale(1.05)", WEATHER_MS),
28091
+ ...swing("weatherIconsRainCloud", "transform", "translate(49 38) scale(1.1)", "translate(51 38) scale(1.1)", WEATHER_MS),
28092
+ ...swing("weatherIconsSnowCloud", "transform", "translate(49 38) scale(1.1)", "translate(51 38) scale(1.1)", WEATHER_MS),
28093
+ ...rain.animate,
28094
+ ...storm.animate,
28095
+ ...snow.animate,
28096
+ // Two quick flashes, then dark until the loop comes round.
28097
+ { id: "weatherIconsBolt", attrs: { opacity: [0, 1] }, delay: 700, ms: 60 },
28098
+ { id: "weatherIconsBolt", attrs: { opacity: [1, 0.2] }, delay: 760, ms: 90 },
28099
+ { id: "weatherIconsBolt", attrs: { opacity: [0.2, 1] }, delay: 850, ms: 60 },
28100
+ { id: "weatherIconsBolt", attrs: { opacity: [1, 0] }, delay: 1100, ms: 400 },
28101
+ // Gusts draw on in turn and fade; undrawn again by the time the loop restarts.
28102
+ ...WIND.flatMap((_, k) => [
28103
+ { id: `weatherIconsWind${k}`, draw: true, delay: k * 220, ms: 900, ease: "in-out" },
28104
+ { id: `weatherIconsWind${k}`, attrs: { opacity: [1, 0] }, delay: 1300 + k * 220, ms: 450 }
28105
+ ])
28106
+ ]
28107
+ }
28108
+ };
28109
+ var BELL_PIVOT = "50 16";
28110
+ var bellTurn = (deg) => `rotate(${deg} ${BELL_PIVOT})`;
28111
+ var BELL_SWING = [0, 18, -15, 11, -7, 3, 0];
28112
+ var CLAPPER_SWING = [0, -12, 14, -9, 6, -2, 0];
28113
+ var BADGE = [72, 26];
28114
+ var notificationBell = {
28115
+ kind: "vector",
28116
+ id: "notification-bell",
28117
+ name: "Notification bell",
28118
+ description: "A bell ringing with a damped swing and its clapper lagging behind, as a badge pops on with the unread count.",
28119
+ category: "icon",
28120
+ tags: ["notification", "bell", "alert", "badge", "ui", "loop"],
28121
+ hold: 300,
28122
+ presets: [
28123
+ { name: "Dark", values: { bell: [0.9, 0.91, 0.94], ring: [0.06, 0.07, 0.1] } },
28124
+ { name: "Many", values: { count: 128 } }
28125
+ ],
28126
+ derived: (v) => {
28127
+ const count = Math.max(0, Math.round(Number(v.count) || 0));
28128
+ const many = count > 99;
28129
+ const digits = many ? 3 : String(count).length;
28130
+ return {
28131
+ from: many ? 99 : Math.max(0, count - 1),
28132
+ to: many ? 99 : count,
28133
+ format: many ? "{n}+" : "{n}",
28134
+ size: digits >= 3 ? 9 : digits === 2 ? 11 : 13,
28135
+ badgeR: digits >= 3 ? 15 : 13
28136
+ };
28137
+ },
28138
+ motion: {
28139
+ svg: `<svg viewBox="0 0 100 100" font-family="${SANS13}"><!-- {{count: int min=0 max=999 default=3 label='Count'}} {{bell: color default=#334155 label='Bell'}} {{badge: color default=#EF4444 label='Badge'}} {{ring: color default=#FFFFFF label='Badge outline'}} --><g fill="none" stroke="{{bell}}" stroke-width="3.5" stroke-linecap="round"><path id="notificationBellWaveL" d="M20 30 C14 38 14 50 20 58" opacity="0"/><path id="notificationBellWaveR" d="M80 30 C86 38 86 50 80 58" opacity="0"/></g><g id="notificationBellClapper" transform="${bellTurn(0)}"><circle cx="50" cy="80" r="7" fill="{{bell}}"/></g><g id="notificationBellBody" transform="${bellTurn(0)}"><circle cx="50" cy="15" r="5" fill="{{bell}}"/><path d="M50 18 C36 18 30 30 30 44 V58 L23 68 V72 H77 V68 L70 58 V44 C70 30 64 18 50 18Z" fill="{{bell}}"/></g><g id="notificationBellBadge" transform="${about6(BADGE[0], BADGE[1], 0)}"><circle cx="${BADGE[0]}" cy="${BADGE[1]}" r="{{badgeR}}" fill="{{badge}}" stroke="{{ring}}" stroke-width="3"/><text id="notificationBellCount" x="${BADGE[0]}" y="${BADGE[1] + 4.5}" text-anchor="middle" font-size="{{size}}" font-weight="800" fill="#FFFFFF">0</text></g></svg>`,
28140
+ animate: [
28141
+ ...BELL_SWING.slice(1).map((deg, k) => ({ id: "notificationBellBody", attrs: { transform: [bellTurn(BELL_SWING[k]), bellTurn(deg)] }, delay: k * 120, ms: 120, ease: "in-out" })),
28142
+ ...CLAPPER_SWING.slice(1).map((deg, k) => ({ id: "notificationBellClapper", attrs: { transform: [bellTurn(CLAPPER_SWING[k]), bellTurn(deg)] }, delay: 50 + k * 120, ms: 120, ease: "in-out" })),
28143
+ ...["L", "R"].flatMap((side, k) => [
28144
+ { id: `notificationBellWave${side}`, attrs: { opacity: [0, 1] }, delay: 80 + k * 120, ms: 120 },
28145
+ { id: `notificationBellWave${side}`, attrs: { opacity: [1, 0] }, delay: 420 + k * 120, ms: 260 }
28146
+ ]),
28147
+ { id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 0), about6(BADGE[0], BADGE[1], 1.25)] }, delay: 300, ms: 180, ease: "out" },
28148
+ { id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 1.25), about6(BADGE[0], BADGE[1], 1)] }, delay: 480, ms: 200, ease: "in-out" },
28149
+ { id: "notificationBellCount", count: [n16("from"), n16("to")], format: "{{format}}", delay: 300, ms: 350, ease: "out" },
28150
+ // The badge goes before the loop starts over, so it can pop on again.
28151
+ { id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 1), about6(BADGE[0], BADGE[1], 0)] }, delay: 2400, ms: 220, ease: "in-out" }
28152
+ ]
28153
+ }
28154
+ };
28155
+ var ARROW_MS = 1200;
28156
+ var refreshArc = (() => {
28157
+ const r = 26;
28158
+ const end = -40 * Math.PI / 180;
28159
+ const [ex, ey] = [50 + r * Math.cos(end), 50 + r * Math.sin(end)];
28160
+ const [tx, ty] = [-Math.sin(end), Math.cos(end)];
28161
+ const [nx, ny] = [Math.cos(end), Math.sin(end)];
28162
+ const tip = `${r115(ex + tx * 7)} ${r115(ey + ty * 7)}`;
28163
+ const head = `M${r115(ex + nx * 7)} ${r115(ey + ny * 7)} L${tip} L${r115(ex - nx * 7)} ${r115(ey - ny * 7)}Z`;
28164
+ return { arc: `M24 50 A${r} ${r} 0 0 1 ${r115(ex)} ${r115(ey)}`, head };
28165
+ })();
28166
+ var animatedArrows = {
28167
+ kind: "vector",
28168
+ id: "animated-arrows",
28169
+ name: "Animated arrows",
28170
+ description: "An arrow icon set: an arrow nudging right, chevrons cascading down, refresh arrows turning, or an external-link arrow leaving its box.",
28171
+ category: "icon",
28172
+ tags: ["arrow", "chevron", "refresh", "link", "ui", "loop"],
28173
+ // Every arrow's motion is over by 1000 ms: each rests the same before it repeats.
28174
+ hold: ARROW_MS - 1e3,
28175
+ presets: [
28176
+ { name: "Scroll hint", values: { kind: 1, color: [0.39, 0.4, 0.95] } },
28177
+ { name: "Refresh", values: { kind: 2 } }
28178
+ ],
28179
+ derived: picks("kind", 4),
28180
+ motion: {
28181
+ svg: `<svg viewBox="0 0 100 100"><!-- {{kind: int min=0 max=3 default=0 label='Arrow (right, chevrons, refresh, external)'}} {{color: color default=#0F172A label='Colour'}} {{width: float min=2 max=12 default=7 step=0.5 label='Stroke width'}} --><g fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"><g display="{{show0}}"><g id="animatedArrowsRight" transform="translate(0 0)"><path d="M20 50 H76 M56 30 L76 50 L56 70"/></g></g><g display="{{show1}}">${[0, 1, 2].map((k) => `<path id="animatedArrowsChevron${k}" d="M32 ${24 + k * 18} L50 ${38 + k * 18} L68 ${24 + k * 18}" opacity="0.2"/>`).join("")}</g><g display="{{show2}}"><g id="animatedArrowsRefresh" transform="rotate(0 50 50)">${[0, 180].map((deg) => `<g transform="rotate(${deg} 50 50)"><path d="${refreshArc.arc}"/><path d="${refreshArc.head}" fill="{{color}}" stroke-width="2"/></g>`).join("")}</g></g><g display="{{show3}}"><path d="M54 22 H32 C26 22 22 26 22 32 V68 C22 74 26 78 32 78 H68 C74 78 78 74 78 68 V46"/><g id="animatedArrowsExternal" transform="translate(0 0)"><path d="M46 54 L76 24 M56 24 H76 V44"/></g></g></g></svg>`,
28182
+ animate: [
28183
+ { id: "animatedArrowsRight", attrs: { transform: ["translate(0 0)", "translate(9 0)"] }, ms: 380, ease: "out" },
28184
+ { id: "animatedArrowsRight", attrs: { transform: ["translate(9 0)", "translate(0 0)"] }, delay: 380, ms: 520, ease: "in-out" },
28185
+ ...[0, 1, 2].flatMap((k) => [
28186
+ { id: `animatedArrowsChevron${k}`, attrs: { opacity: [0.2, 1], transform: ["translate(0 0)", "translate(0 3)"] }, delay: k * 180, ms: 260, ease: "out" },
28187
+ { id: `animatedArrowsChevron${k}`, attrs: { opacity: [1, 0.2], transform: ["translate(0 3)", "translate(0 0)"] }, delay: k * 180 + 300, ms: 420, ease: "in-out" }
28188
+ ]),
28189
+ // Two arrows round the circle: half a turn brings it back to how it started.
28190
+ { id: "animatedArrowsRefresh", attrs: { transform: ["rotate(0 50 50)", "rotate(180 50 50)"] }, ms: 1e3, ease: "in-out" },
28191
+ { id: "animatedArrowsExternal", attrs: { transform: ["translate(0 0)", "translate(10 -10)"], opacity: [1, 0] }, delay: 100, ms: 360, ease: "in-out" },
28192
+ { id: "animatedArrowsExternal", attrs: { transform: ["translate(-10 10)", "translate(0 0)"], opacity: [0, 1] }, delay: 520, ms: 420, ease: "out" }
28193
+ ]
28194
+ }
28195
+ };
28196
+ var EMOJI_Y = 40;
28197
+ var emojiX = (i) => 30 + i * 44;
28198
+ var emojiPose = (i, dy, s) => `translate(${emojiX(i)} ${EMOJI_Y}) translate(0 ${dy}) scale(${s})`;
28199
+ var FACES = [
28200
+ // Like: a thumb up on a blue disc.
28201
+ `<circle r="17" fill="{{like}}"/><path d="M-8 -1 H-4 V9 H-8Z M-2 -1 L2 -9 C3.5 -10.5 6.5 -9 5.8 -6.3 L4.8 -2.5 H8.5 C10.4 -2.5 11.4 -0.6 10.8 1.2 L8.8 7.5 C8.3 9 7.2 9.6 5.8 9.6 H-2Z" fill="#FFFFFF"/>`,
28202
+ // Love: a heart on a red disc.
28203
+ `<circle r="17" fill="{{love}}"/><path d="M0 9 C0 9 -9.5 3 -9.5 -2.8 C-9.5 -6 -7.3 -8.2 -4.6 -8.2 C-2.6 -8.2 -0.9 -7.1 0 -5.5 C0.9 -7.1 2.6 -8.2 4.6 -8.2 C7.3 -8.2 9.5 -6 9.5 -2.8 C9.5 3 0 9 0 9Z" fill="#FFFFFF"/>`,
28204
+ // Haha: shut eyes and a wide open laugh.
28205
+ `<circle r="17" fill="{{face}}"/><path d="M-10 -6 L-5 -3.5 L-10 -1 M10 -6 L5 -3.5 L10 -1" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M-9 2 H9 C9 8.5 5 12 0 12 C-5 12 -9 8.5 -9 2Z" fill="{{features}}"/>`,
28206
+ // Wow: raised brows, round eyes and mouth.
28207
+ `<circle r="17" fill="{{face}}"/><path d="M-9 -9 Q-6 -11.5 -3 -9.5 M9 -9 Q6 -11.5 3 -9.5" fill="none" stroke="{{features}}" stroke-width="1.8" stroke-linecap="round"/><ellipse cx="-5.5" cy="-3.5" rx="2.2" ry="3" fill="{{features}}"/><ellipse cx="5.5" cy="-3.5" rx="2.2" ry="3" fill="{{features}}"/><ellipse cx="0" cy="7.5" rx="4" ry="5" fill="{{features}}"/>`,
28208
+ // Sad: a frown and a tear.
28209
+ `<circle r="17" fill="{{face}}"/><circle cx="-5.5" cy="-3" r="2.2" fill="{{features}}"/><circle cx="5.5" cy="-3" r="2.2" fill="{{features}}"/><path d="M-6 9.5 Q0 3.5 6 9.5" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round"/><path d="M-6 1 C-4.5 3.5 -4 4.8 -4 5.8 A2 2 0 0 1 -8 5.8 C-8 4.8 -7.5 3.5 -6 1Z" fill="#38BDF8"/>`,
28210
+ // Angry: a red-orange face with knitted brows.
28211
+ `<circle r="17" fill="{{angry}}"/><path d="M-10 -8 L-3 -4.5 M10 -8 L3 -4.5" stroke="{{features}}" stroke-width="2.2" stroke-linecap="round"/><circle cx="-5" cy="-1" r="2" fill="{{features}}"/><circle cx="5" cy="-1" r="2" fill="{{features}}"/><path d="M-6 10 Q0 5.5 6 10" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round"/>`
28212
+ ];
28213
+ var emojiReactions = {
28214
+ kind: "vector",
28215
+ id: "emoji-reactions",
28216
+ name: "Emoji reactions",
28217
+ description: "A reaction picker: on hover the bar opens and like, love, haha, wow, sad and angry faces bounce up one after another, drawn as vectors.",
28218
+ category: "icon",
28219
+ tags: ["interactive", "hover", "emoji", "reactions", "social", "ui"],
28220
+ trigger: "hover",
28221
+ hold: 600,
28222
+ presets: [{ name: "Dark", values: { pill: [0.13, 0.14, 0.19], button: [0.6, 0.63, 0.7] } }],
28223
+ motion: {
28224
+ svg: `<svg viewBox="0 0 280 80"><!-- {{pill: color default=#FFFFFF label='Bar'}} {{button: color default=#64748B label='Button icon'}} {{face: color default=#FBBF24 label='Faces'}} {{features: color default=#5B3A00 label='Eyes and mouths'}} {{like: color default=#2563EB label='Like'}} {{love: color default=#F43F5E label='Love'}} {{angry: color default=#F97316 label='Angry'}} --><rect id="emojiReactionsShadow" x="8" y="17" width="52" height="52" rx="26" fill="#000000" opacity="0.1"/><rect id="emojiReactionsPill" x="6" y="14" width="52" height="52" rx="26" fill="{{pill}}" stroke="#000000" stroke-opacity="0.08"/><g id="emojiReactionsButton" fill="none" stroke="{{button}}" stroke-width="2.2" stroke-linecap="round"><circle cx="30" cy="40" r="11"/><circle cx="26" cy="37" r="0.8" fill="{{button}}"/><circle cx="34" cy="37" r="0.8" fill="{{button}}"/><path d="M25.5 43.5 Q30 47 34.5 43.5"/></g>${FACES.map(
28225
+ (face, i) => `<g id="emojiReactionsFace${i}" transform="${emojiPose(i, 14, 0)}">${face}</g>`
28226
+ ).join("")}</svg>`,
28227
+ animate: [
28228
+ { id: "emojiReactionsPill", attrs: { width: [52, 268] }, ms: 280, ease: "out" },
28229
+ { id: "emojiReactionsShadow", attrs: { width: [52, 268] }, ms: 280, ease: "out" },
28230
+ { id: "emojiReactionsButton", attrs: { opacity: [1, 0] }, ms: 120 },
28231
+ ...FACES.flatMap((_, i) => [
28232
+ { id: `emojiReactionsFace${i}`, attrs: { transform: [emojiPose(i, 14, 0), emojiPose(i, -9, 1.15)] }, delay: 90 + i * 70, ms: 200, ease: "out" },
28233
+ { id: `emojiReactionsFace${i}`, attrs: { transform: [emojiPose(i, -9, 1.15), emojiPose(i, 0, 1)] }, delay: 290 + i * 70, ms: 200, ease: "in-out" }
28234
+ ])
28235
+ ]
28236
+ }
28237
+ };
28238
+ var STARS3 = 5;
28239
+ var STAR = { y: 32, r: 16, inner: 7, pitch: 38, first: 22 };
28240
+ var starX = (i) => STAR.first + i * STAR.pitch;
28241
+ var starPath2 = (cx) => `M${Array.from({ length: 10 }, (_, k) => {
28242
+ const a = k * Math.PI / 5 - Math.PI / 2;
28243
+ const r = k % 2 ? STAR.inner : STAR.r;
28244
+ return `${r115(cx + r * Math.cos(a))} ${r115(STAR.y + 1.5 + r * Math.sin(a))}`;
28245
+ }).join(" L")}Z`;
28246
+ var bookmarkStar = {
28247
+ kind: "vector",
28248
+ id: "bookmark-star",
28249
+ name: "Star rating",
28250
+ description: "Five stars that fill one after another on hover, each bouncing as it fills (halves too), while the rating counts up beside them.",
28251
+ category: "icon",
28252
+ tags: ["interactive", "hover", "rating", "stars", "review", "ui"],
28253
+ trigger: "hover",
28254
+ hold: 600,
28255
+ presets: [
28256
+ { name: "Half", values: { rating: 3.5 } },
28257
+ { name: "Dark", values: { empty: [0.28, 0.31, 0.37], ink: [0.94, 0.95, 0.97] } }
28258
+ ],
28259
+ derived: (v) => {
28260
+ const rating = Math.min(STARS3, Math.max(0, Math.round((Number(v.rating) || 0) * 2) / 2));
28261
+ return {
28262
+ rating,
28263
+ ...Object.fromEntries(
28264
+ Array.from({ length: STARS3 }, (_, i) => {
28265
+ const f = Math.min(1, Math.max(0, rating - i));
28266
+ return [
28267
+ [`w${i}`, r115(f * STAR.r * 2)],
28268
+ [`pop${i}`, about6(starX(i), STAR.y, f > 0 ? 1.22 : 1)]
28269
+ ];
28270
+ }).flat()
28271
+ )
28272
+ };
28273
+ },
28274
+ motion: {
28275
+ svg: `<svg viewBox="0 0 260 64" font-family="${SANS13}"><!-- {{rating: float min=0 max=5 default=4.5 step=0.5 label='Rating'}} {{star: color default=#FBBF24 label='Filled'}} {{empty: color default=#CBD5E1 label='Empty'}} {{ink: color default=#0F172A label='Text'}} --><defs>${Array.from(
28276
+ { length: STARS3 },
28277
+ (_, i) => `<clipPath id="bookmarkStarClip${i}"><rect id="bookmarkStarFill${i}" x="${starX(i) - STAR.r}" y="${STAR.y - STAR.r - 2}" width="0" height="${STAR.r * 2 + 6}"/></clipPath>`
28278
+ ).join("")}</defs>${Array.from(
28279
+ { length: STARS3 },
28280
+ (_, i) => `<g id="bookmarkStarStar${i}" transform="${about6(starX(i), STAR.y, 1)}"><path d="${starPath2(starX(i))}" fill="{{empty}}" stroke="{{empty}}" stroke-width="2" stroke-linejoin="round"/><path d="${starPath2(starX(i))}" fill="{{star}}" stroke="{{star}}" stroke-width="2" stroke-linejoin="round" clip-path="url(#bookmarkStarClip${i})"/></g>`
28281
+ ).join("")}<text id="bookmarkStarValue" x="202" y="38.5" font-size="16" font-weight="700" fill="{{ink}}" opacity="0">0.0</text></svg>`,
28282
+ animate: [
28283
+ ...Array.from({ length: STARS3 }, (_, i) => [
28284
+ { id: `bookmarkStarFill${i}`, attrs: { width: [0, n16(`w${i}`)] }, delay: i * 110, ms: 160, ease: "linear" },
28285
+ { id: `bookmarkStarStar${i}`, attrs: { transform: [about6(starX(i), STAR.y, 1), `{{pop${i}}}`] }, delay: i * 110 + 60, ms: 130, ease: "out" },
28286
+ { id: `bookmarkStarStar${i}`, attrs: { transform: [`{{pop${i}}}`, about6(starX(i), STAR.y, 1)] }, delay: i * 110 + 190, ms: 180, ease: "in-out" }
28287
+ ]).flat(),
28288
+ { id: "bookmarkStarValue", attrs: { opacity: [0, 1] }, ms: 200 },
28289
+ { id: "bookmarkStarValue", count: [0, n16("rating")], decimals: 1, format: "{n} / 5", ms: STARS3 * 110 + 100, ease: "out" }
28290
+ ]
28291
+ }
28292
+ };
28293
+ var LID_HINGE = "76 30";
28294
+ var lid = (deg) => `rotate(${deg} ${LID_HINGE})`;
28295
+ var paperAt = (x, y, deg) => `translate(${x} ${y}) rotate(${deg})`;
28296
+ var canTilt = (deg) => `rotate(${deg} 50 88)`;
28297
+ var trashDelete = {
28298
+ kind: "vector",
28299
+ id: "trash-delete",
28300
+ name: "Trash delete",
28301
+ description: "A trash can whose lid flips open as a crumpled paper drops in, then snaps shut with a little wobble; replays on every click.",
28302
+ category: "icon",
28303
+ tags: ["interactive", "click", "delete", "trash", "remove", "ui"],
28304
+ trigger: "click",
28305
+ hold: 600,
28306
+ presets: [
28307
+ { name: "Danger", values: { color: [0.86, 0.15, 0.15] } },
28308
+ { name: "Dark", values: { color: [0.9, 0.91, 0.94], paper: [0.6, 0.63, 0.7] } }
28309
+ ],
28310
+ motion: {
28311
+ svg: `<svg viewBox="0 0 100 100"><!-- {{color: color default=#334155 label='Can'}} {{paper: color default=#F8FAFC label='Paper'}} {{width: float min=2 max=8 default=5 step=0.5 label='Stroke width'}} --><g id="trashDeletePaper" transform="${paperAt(38, 12, 0)}" opacity="0"><path d="M-9 -4 L-4 -10 L3 -8 L9 -3 L8 5 L2 9 L-6 8 L-10 2Z" fill="{{paper}}" stroke="{{color}}" stroke-width="2" stroke-linejoin="round"/><path d="M-4 -3 L2 1 M-2 5 L4 -2" stroke="{{color}}" stroke-width="1.4" stroke-linecap="round"/></g><g id="trashDeleteCan" transform="${canTilt(0)}" fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"><path d="M30 36 L34 82 C34.4 86 36 88 40 88 H60 C64 88 65.6 86 66 82 L70 36"/><path d="M43 46 V76 M57 46 V76"/><g id="trashDeleteLid" transform="${lid(0)}"><path d="M24 30 H76 M42 30 V23 H58 V30"/></g></g></svg>`,
28312
+ animate: [
28313
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(0), lid(32)] }, ms: 220, ease: "out" },
28314
+ { id: "trashDeletePaper", step: 1, attrs: { opacity: [0, 1] }, delay: 100, ms: 100 },
28315
+ { id: "trashDeletePaper", step: 1, attrs: { transform: [paperAt(38, 12, 0), paperAt(48, 62, 160)] }, delay: 120, ms: 420, ease: "in-out" },
28316
+ { id: "trashDeletePaper", step: 1, attrs: { opacity: [1, 0] }, delay: 430, ms: 110 },
28317
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(32), lid(0)] }, delay: 540, ms: 170, ease: "in-out" },
28318
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(0), lid(5)] }, delay: 710, ms: 80, ease: "out" },
28319
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(5), lid(0)] }, delay: 790, ms: 120, ease: "in-out" },
28320
+ { id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(0), canTilt(-5)] }, delay: 700, ms: 90, ease: "out" },
28321
+ { id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(-5), canTilt(3)] }, delay: 790, ms: 120, ease: "in-out" },
28322
+ { id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(3), canTilt(0)] }, delay: 910, ms: 140, ease: "in-out" },
28323
+ // The paper, gone, goes back to the top for the next click.
28324
+ { id: "trashDeletePaper", step: 1, attrs: { transform: [paperAt(38, 12, 0), paperAt(38, 12, 0)] }, delay: 1100, ms: 0 }
28325
+ ]
28326
+ }
28327
+ };
28328
+ var shackle = (y) => `translate(0 ${y})`;
28329
+ var keyPose = (deg, s) => `translate(50 66) rotate(${deg}) scale(${s})`;
28330
+ var lockUnlock = {
28331
+ kind: "vector",
28332
+ id: "lock-unlock",
28333
+ name: "Lock and unlock",
28334
+ description: "A padlock: a click turns a key and pops the shackle open as the lock turns green; the next click turns it back and snaps it shut.",
28335
+ category: "icon",
28336
+ tags: ["interactive", "click", "lock", "security", "toggle", "ui"],
28337
+ trigger: "click",
28338
+ hold: 600,
28339
+ presets: [{ name: "Dark", values: { locked: [0.85, 0.87, 0.9], hole: [0.06, 0.07, 0.1] } }],
28340
+ motion: {
28341
+ svg: `<svg viewBox="0 0 100 100"><!-- {{locked: color default=#334155 label='Locked'}} {{open: color default=#16A34A label='Unlocked'}} {{hole: color default=#FFFFFF label='Keyhole'}} {{key: color default=#F59E0B label='Key'}} --><g id="lockUnlockShackle" transform="${shackle(0)}"><path id="lockUnlockLoop" d="M34 48 V34 C34 25 41 18 50 18 C59 18 66 25 66 34 V48" fill="none" stroke="{{locked}}" stroke-width="8"/></g><rect id="lockUnlockBody" x="22" y="44" width="56" height="44" rx="9" fill="{{locked}}"/><g id="lockUnlockHole" transform="rotate(0 50 66)"><circle cx="50" cy="62" r="5.5" fill="{{hole}}"/><path d="M47.5 64 H52.5 L53.5 75 H46.5Z" fill="{{hole}}"/></g><g id="lockUnlockKey" transform="${keyPose(0, 0.4)}" opacity="0"><rect x="-2.5" y="-6" width="5" height="10" fill="{{key}}"/><circle cy="-13" r="8" fill="none" stroke="{{key}}" stroke-width="4.5"/></g></svg>`,
28342
+ animate: [
28343
+ // Step 1, unlock: the key goes in and turns, the shackle springs up, and the lock goes green.
28344
+ { id: "lockUnlockKey", step: 1, attrs: { opacity: [0, 1], transform: [keyPose(0, 0.4), keyPose(0, 1)] }, ms: 160, ease: "out" },
28345
+ { id: "lockUnlockKey", step: 1, attrs: { transform: [keyPose(0, 1), keyPose(90, 1)] }, delay: 180, ms: 200, ease: "in-out" },
28346
+ { id: "lockUnlockHole", step: 1, attrs: { transform: ["rotate(0 50 66)", "rotate(90 50 66)"] }, delay: 180, ms: 200, ease: "in-out" },
28347
+ { id: "lockUnlockShackle", step: 1, attrs: { transform: [shackle(0), shackle(-16)] }, delay: 380, ms: 160, ease: "out" },
28348
+ { id: "lockUnlockShackle", step: 1, attrs: { transform: [shackle(-16), shackle(-12)] }, delay: 540, ms: 180, ease: "in-out" },
28349
+ { id: "lockUnlockBody", step: 1, attrs: { fill: ["{{locked}}", "{{open}}"] }, delay: 380, ms: 220 },
28350
+ { id: "lockUnlockLoop", step: 1, attrs: { stroke: ["{{locked}}", "{{open}}"] }, delay: 380, ms: 220 },
28351
+ { id: "lockUnlockKey", step: 1, attrs: { opacity: [1, 0] }, delay: 650, ms: 200 },
28352
+ // Step 2, lock: the key turns back, the shackle drops shut with a bump.
28353
+ { id: "lockUnlockKey", step: 2, attrs: { opacity: [0, 1], transform: [keyPose(90, 0.4), keyPose(90, 1)] }, ms: 160, ease: "out" },
28354
+ { id: "lockUnlockKey", step: 2, attrs: { transform: [keyPose(90, 1), keyPose(0, 1)] }, delay: 180, ms: 200, ease: "in-out" },
28355
+ { id: "lockUnlockHole", step: 2, attrs: { transform: ["rotate(90 50 66)", "rotate(0 50 66)"] }, delay: 180, ms: 200, ease: "in-out" },
28356
+ { id: "lockUnlockShackle", step: 2, attrs: { transform: [shackle(-12), shackle(2)] }, delay: 380, ms: 150, ease: "in-out" },
28357
+ { id: "lockUnlockShackle", step: 2, attrs: { transform: [shackle(2), shackle(0)] }, delay: 530, ms: 120, ease: "out" },
28358
+ { id: "lockUnlockBody", step: 2, attrs: { fill: ["{{open}}", "{{locked}}"] }, delay: 400, ms: 220 },
28359
+ { id: "lockUnlockLoop", step: 2, attrs: { stroke: ["{{open}}", "{{locked}}"] }, delay: 400, ms: 220 },
28360
+ { id: "lockUnlockKey", step: 2, attrs: { opacity: [1, 0] }, delay: 650, ms: 200 },
28361
+ { id: "lockUnlockKey", step: 2, attrs: { transform: [keyPose(0, 0.4), keyPose(0, 0.4)] }, delay: 900, ms: 0 }
28362
+ ]
28363
+ }
28364
+ };
28365
+ var WIFI = { cx: 50, cy: 80 };
28366
+ var wifiArc = (r) => {
28367
+ const d = r * Math.SQRT1_2;
28368
+ return `M${r115(WIFI.cx - d)} ${r115(WIFI.cy - d)} A${r} ${r} 0 0 1 ${r115(WIFI.cx + d)} ${r115(WIFI.cy - d)}`;
28369
+ };
28370
+ var WIFI_ARCS = [20, 36, 52];
28371
+ var wifiSignal = {
28372
+ kind: "vector",
28373
+ id: "wifi-signal",
28374
+ name: "Wi-Fi signal",
28375
+ description: "Wi-Fi bars filling one after another from the dot outward, pulsing twice at full strength, then fading to start again.",
28376
+ category: "icon",
28377
+ tags: ["wifi", "signal", "connection", "network", "status", "loop"],
28378
+ hold: 200,
28379
+ presets: [
28380
+ { name: "Dark", values: { track: [0.2, 0.22, 0.27], color: [0.38, 0.65, 0.98] } }
28381
+ ],
28382
+ motion: {
28383
+ svg: `<svg viewBox="0 0 100 100"><!-- {{color: color default=#2563EB label='Signal'}} {{track: color default=#E2E8F0 label='Empty bars'}} {{width: float min=4 max=12 default=9 step=0.5 label='Bar width'}} --><g fill="none" stroke="{{track}}" stroke-width="{{width}}" stroke-linecap="round">${WIFI_ARCS.map((r) => `<path d="${wifiArc(r)}"/>`).join("")}</g><circle cx="${WIFI.cx}" cy="${WIFI.cy}" r="6.5" fill="{{track}}"/><g id="wifiSignalOn"><circle id="wifiSignalDot" cx="${WIFI.cx}" cy="${WIFI.cy}" r="6.5" fill="{{color}}" opacity="0"/><g fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round">${WIFI_ARCS.map((r, k) => `<path id="wifiSignalArc${k}" d="${wifiArc(r)}" opacity="0"/>`).join("")}</g></g></svg>`,
28384
+ animate: [
28385
+ { id: "wifiSignalDot", attrs: { opacity: [0, 1], r: [3, 6.5] }, ms: 220, ease: "out" },
28386
+ ...WIFI_ARCS.map((_, k) => ({ id: `wifiSignalArc${k}`, attrs: { opacity: [0, 1] }, delay: 250 + k * 250, ms: 220, ease: "out" })),
28387
+ ...[0, 1].flatMap((k) => swing("wifiSignalOn", "opacity", 1, 0.45, 600, 1150 + k * 600)),
28388
+ // Fades out together, so the loop starts from empty again.
28389
+ { id: "wifiSignalDot", attrs: { opacity: [1, 0], r: [6.5, 3] }, delay: 2600, ms: 300 },
28390
+ ...WIFI_ARCS.map((_, k) => ({ id: `wifiSignalArc${k}`, attrs: { opacity: [1, 0] }, delay: 2600, ms: 300 }))
28391
+ ]
28392
+ }
28393
+ };
28394
+ var iconEffects = [weatherIcons, notificationBell, animatedArrows, emojiReactions, bookmarkStar, trashDelete, lockUnlock, wifiSignal];
28395
+
27349
28396
  // src/catalog/index.ts
27350
28397
  var SHADER_EFFECTS = [
27351
28398
  meshGradient,
@@ -27422,7 +28469,7 @@ var SHADER_EFFECTS = [
27422
28469
  ...waterShaders,
27423
28470
  ...transitions
27424
28471
  ];
27425
- var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects, ...broadcastEffects, ...annotationEffects, ...uiEffects, ...loaderEffects].sort(
28472
+ var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects, ...broadcastEffects, ...annotationEffects, ...uiEffects, ...loaderEffects, ...dataEffects, ...iconEffects].sort(
27426
28473
  (a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
27427
28474
  );
27428
28475
  var byId = new Map(EFFECTS.map((e) => [e.id, e]));
@@ -27439,6 +28486,7 @@ function findEffects(query = {}) {
27439
28486
  const words = (query.text ?? "").toLowerCase().split(/\s+/).filter(Boolean);
27440
28487
  return EFFECTS.filter((e) => {
27441
28488
  if (query.category && e.category !== query.category) return false;
28489
+ if (query.group && !EFFECT_CATEGORY_GROUPS.find((g) => g.name.toLowerCase() === query.group.toLowerCase())?.categories.includes(e.category)) return false;
27442
28490
  if (query.kind && e.kind !== query.kind) return false;
27443
28491
  if (query.tags && !query.tags.every((t) => e.tags.includes(t))) return false;
27444
28492
  if (query.video && !(e.kind === "shader" && e.video && query.video.includes(e.video))) return false;
@@ -27479,6 +28527,8 @@ export {
27479
28527
  SAMPLE_IMAGES,
27480
28528
  textImage,
27481
28529
  EFFECT_CATEGORIES,
28530
+ EFFECT_CATEGORY_GROUPS,
28531
+ categoryGroup,
27482
28532
  VIDEO_USES,
27483
28533
  VECTOR_CONTROL_TYPES,
27484
28534
  vectorControls,
@@ -27498,4 +28548,4 @@ export {
27498
28548
  effectControls,
27499
28549
  effectDefaults
27500
28550
  };
27501
- //# sourceMappingURL=chunk-RS2S2K5C.js.map
28551
+ //# sourceMappingURL=chunk-IHUNCBMF.js.map