@pixerate/flair 0.1.1 → 0.3.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 (44) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +65 -14
  3. package/dist/catalog/index.d.ts +5 -4
  4. package/dist/catalog/index.js +12 -4
  5. package/dist/{chunk-JWZCPMFG.js → chunk-D7NLHC7K.js} +117 -674
  6. package/dist/chunk-D7NLHC7K.js.map +1 -0
  7. package/dist/{chunk-3FAUZOGL.js → chunk-FIJOMGPG.js} +2 -2
  8. package/dist/{chunk-UQQNZSYL.js → chunk-GTIAOYOK.js} +211 -7
  9. package/dist/chunk-GTIAOYOK.js.map +1 -0
  10. package/dist/{chunk-MWVTQFV5.js → chunk-IWKM5FJK.js} +410 -59
  11. package/dist/chunk-IWKM5FJK.js.map +1 -0
  12. package/dist/{chunk-HKXOHRCD.js → chunk-VA74HBPK.js} +2 -2
  13. package/dist/chunk-WAMA45G3.js +998 -0
  14. package/dist/chunk-WAMA45G3.js.map +1 -0
  15. package/dist/{chunk-J55UBNAT.js → chunk-WC3VADZC.js} +42 -36
  16. package/dist/chunk-WC3VADZC.js.map +1 -0
  17. package/dist/cli.js +12 -11
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{controls-DdXMbovi.d.ts → controls-Syr6drzO.d.ts} +4 -3
  20. package/dist/editor/index.d.ts +101 -49
  21. package/dist/editor/index.js +198 -99
  22. package/dist/editor/index.js.map +1 -1
  23. package/dist/{export-FEBPEEO7.js → export-DH2HMACO.js} +4 -3
  24. package/dist/index.d.ts +193 -17
  25. package/dist/index.js +69 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/node.d.ts +1 -1
  28. package/dist/node.js +2 -2
  29. package/dist/recorder-KEX3U35D.js +10 -0
  30. package/dist/{renderer-Oyynz-fs.d.ts → renderer-mzvhKzKa.d.ts} +1 -1
  31. package/dist/types-CsFUZZIv.d.ts +102 -0
  32. package/package.json +3 -3
  33. package/dist/chunk-36DTNSHU.js +0 -160
  34. package/dist/chunk-36DTNSHU.js.map +0 -1
  35. package/dist/chunk-J55UBNAT.js.map +0 -1
  36. package/dist/chunk-JWZCPMFG.js.map +0 -1
  37. package/dist/chunk-MWVTQFV5.js.map +0 -1
  38. package/dist/chunk-UQQNZSYL.js.map +0 -1
  39. package/dist/recorder-WJTWMEQN.js +0 -10
  40. package/dist/types-CkKkavvB.d.ts +0 -71
  41. /package/dist/{chunk-3FAUZOGL.js.map → chunk-FIJOMGPG.js.map} +0 -0
  42. /package/dist/{chunk-HKXOHRCD.js.map → chunk-VA74HBPK.js.map} +0 -0
  43. /package/dist/{export-FEBPEEO7.js.map → export-DH2HMACO.js.map} +0 -0
  44. /package/dist/{recorder-WJTWMEQN.js.map → recorder-KEX3U35D.js.map} +0 -0
@@ -2,11 +2,13 @@ import {
2
2
  getTransitionGLSL
3
3
  } from "./chunk-EHDJ3CPJ.js";
4
4
  import {
5
+ checkControlValue,
6
+ controlFromDeclaration,
5
7
  controlValue,
6
8
  hexToRgb,
7
9
  parseControls,
8
10
  rgbToHex
9
- } from "./chunk-J55UBNAT.js";
11
+ } from "./chunk-WC3VADZC.js";
10
12
 
11
13
  // src/catalog/glsl.ts
12
14
  var HASH = `
@@ -806,6 +808,60 @@ uniform float uSpin; // @control min=0 max=4 default=1 label="Spin"`,
806
808
  }`
807
809
  )
808
810
  };
811
+ var lightRibbons = {
812
+ kind: "shader",
813
+ id: "light-ribbons",
814
+ name: "Light ribbons",
815
+ description: "A bundle of thin iridescent light strands weaving across a dark frame.",
816
+ category: "light",
817
+ tags: ["background", "hero", "tech", "iridescent", "flow"],
818
+ source: glsl(
819
+ `// Light ribbons: thin glowing strands weaving across the frame, shading from one colour to the next.
820
+ uniform vec3 uBackground; // @control color default=#05060D label="Background"
821
+ uniform vec3 uColorA; // @control color default=#4FD1FF label="Colour A"
822
+ uniform vec3 uColorB; // @control color default=#B06BFF label="Colour B"
823
+ uniform vec3 uColorC; // @control color default=#FF7AC6 label="Colour C"
824
+ uniform float uCount; // @control min=2 max=24 default=12 step=1 label="Strands"
825
+ uniform float uSpread; // @control min=0.02 max=0.6 default=0.18 label="Spread"
826
+ uniform float uWave; // @control min=0 max=0.5 default=0.22 label="Wave"
827
+ uniform float uTilt; // @control min=-1 max=1 default=0.3 label="Tilt"
828
+ uniform float uSpeed; // @control min=0 max=2 default=0.5 label="Speed"
829
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow"`,
830
+ `// Three colours round a loop, so the shading drifts without a seam.
831
+ vec3 shade(float g) {
832
+ g = fract(g) * 3.0;
833
+ return g < 1.0 ? mix(uColorA, uColorB, g) : g < 2.0 ? mix(uColorB, uColorC, g - 1.0) : mix(uColorC, uColorA, g - 2.0);
834
+ }
835
+
836
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
837
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
838
+ float px = 1.0 / iResolution.y;
839
+ float t = iTime * uSpeed;
840
+ float halfWidth = 0.5 * iResolution.x / iResolution.y;
841
+ float x = p.x;
842
+ float y = p.y - x * uTilt;
843
+ // The bundle pinches and opens along its length, like ribbons twisting.
844
+ float pinch = 0.35 + 0.65 * abs(sin(x * 1.3 - t * 0.7));
845
+ vec3 light = vec3(0.0);
846
+ for (int i = 0; i < 24; i++) {
847
+ if (float(i) >= uCount) break;
848
+ float f = float(i) / max(uCount - 1.0, 1.0) - 0.5;
849
+ float centre = f * uSpread * pinch
850
+ + uWave * (0.6 * sin(x * 1.7 + t + f * 2.0) + 0.3 * sin(x * 3.3 - t * 1.3 + f * 5.0));
851
+ float d = abs(y - centre);
852
+ float core = 1.0 - smoothstep(0.0, px * 1.5, d);
853
+ float glow = uGlow * 0.006 / (d + 0.006);
854
+ light += shade(f * 0.6 + x * 0.12 + t * 0.05) * (core * 0.9 + glow * 0.4);
855
+ }
856
+ light *= 1.0 - 0.75 * pow(min(abs(x) / halfWidth, 1.0), 2.0);
857
+ fragColor = vec4(uBackground + (1.0 - exp(-light)), 1.0);
858
+ }`
859
+ ),
860
+ presets: [
861
+ { name: "Gold", values: { uBackground: [0.03, 0.03, 0.04], uColorA: [1, 0.78, 0.4], uColorB: [0.75, 0.5, 0.2], uColorC: [1, 0.93, 0.75], uCount: 8, uSpread: 0.4 } },
862
+ { name: "Aurora", values: { uColorA: [0.2, 1, 0.7], uColorB: [0.2, 0.6, 1], uColorC: [0.6, 0.3, 1], uTilt: -0.2, uWave: 0.3 } }
863
+ ]
864
+ };
809
865
 
810
866
  // src/catalog/shaders/organic.ts
811
867
  var marble = {
@@ -1234,6 +1290,119 @@ uniform vec3 uTint; // @control color default=#FFFFFF label="Tint"`,
1234
1290
  }`
1235
1291
  )
1236
1292
  };
1293
+ var neonCircuit = {
1294
+ kind: "shader",
1295
+ id: "neon-circuit",
1296
+ name: "Neon circuit",
1297
+ description: "Glowing neon traces routed up a circuit board, with pulses of light racing along them.",
1298
+ category: "pattern",
1299
+ tags: ["background", "neon", "circuit", "tech", "80s", "vaporwave"],
1300
+ source: glsl(
1301
+ `// Neon circuit: glowing traces climbing a board in rounded jogs, with pulses running along them.
1302
+ uniform vec3 uBackground; // @control color default=#170A2E label="Background"
1303
+ uniform vec3 uColorA; // @control color default=#FF5FA2 label="Trace A"
1304
+ uniform vec3 uColorB; // @control color default=#43E5FF label="Trace B"
1305
+ uniform vec3 uColorC; // @control color default=#FFD23F label="Trace C"
1306
+ uniform vec3 uColorD; // @control color default=#9D7BFF label="Trace D"
1307
+ uniform float uLanes; // @control min=4 max=40 default=14 label="Lanes"
1308
+ uniform float uDensity; // @control min=0.1 max=1 default=0.7 label="Density"
1309
+ uniform float uWidth; // @control min=0.04 max=0.4 default=0.16 label="Line width"
1310
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow"
1311
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Pulse speed"
1312
+ uniform float uPulses; // @control min=0 max=1 default=0.5 label="Pulses"
1313
+ uniform float uScroll; // @control min=-2 max=2 default=0.4 label="Scroll"
1314
+ uniform float uGrid; // @control min=0 max=1 default=0.3 label="Grid"`,
1315
+ HASH,
1316
+ `// Each trace k owns lanes 3k..3k+2 and climbs in runs of RUN lanes, jogging
1317
+ // sideways between them on rounded corners, so traces never cross.
1318
+ const float RUN = 2.5;
1319
+ const float RADIUS = 0.35;
1320
+
1321
+ float laneOf(float k, float j) {
1322
+ return 3.0 * k + floor(hash12(vec2(k, j) + 0.37) * 3.0) + 0.5;
1323
+ }
1324
+
1325
+ vec3 traceColor(float k) {
1326
+ float h = hash12(vec2(k, 7.3));
1327
+ return h < 0.25 ? uColorA : h < 0.5 ? uColorB : h < 0.75 ? uColorC : uColorD;
1328
+ }
1329
+
1330
+ // Distance to a quarter circle of RADIUS round c, in the quadrant whose signs are q.
1331
+ float arcDist(vec2 p, vec2 c, vec2 q) {
1332
+ vec2 d = p - c;
1333
+ return d.x * q.x >= 0.0 && d.y * q.y >= 0.0 ? abs(length(d) - RADIUS) : 1e5;
1334
+ }
1335
+
1336
+ // Trace k's run j: distance from p to its climb (x) and how far up it p is (y), then
1337
+ // the same for its jog into run j + 1 (z, w), measured along the run.
1338
+ vec4 run(vec2 p, float k, float j) {
1339
+ float x0 = laneOf(k, j - 1.0);
1340
+ float x1 = laneOf(k, j);
1341
+ float x2 = laneOf(k, j + 1.0);
1342
+ float y0 = j * RUN;
1343
+ float y1 = y0 + RUN;
1344
+ float lo = y0 + (x0 == x1 ? 0.0 : RADIUS);
1345
+ float hi = y1 - (x2 == x1 ? 0.0 : RADIUS);
1346
+ vec4 r = vec4(length(p - vec2(x1, clamp(p.y, lo, hi))), p.y - y0, 1e5, 0.0);
1347
+ if (x2 != x1) {
1348
+ float sx = sign(x2 - x1);
1349
+ float h = length(p - vec2(clamp(p.x, min(x1, x2) + RADIUS, max(x1, x2) - RADIUS), y1));
1350
+ h = min(h, arcDist(p, vec2(x1 + sx * RADIUS, y1 - RADIUS), vec2(-sx, 1.0)));
1351
+ h = min(h, arcDist(p, vec2(x2 - sx * RADIUS, y1 + RADIUS), vec2(sx, -1.0)));
1352
+ r.zw = vec2(h, RUN + abs(p.x - x1));
1353
+ }
1354
+ return r;
1355
+ }
1356
+
1357
+ // How bright a pulse is at s along run j of trace k: a bright head with a fading tail.
1358
+ // A pulse crosses one run per cycle and carries on into the next with the next cycle.
1359
+ float pulseAt(float k, float j, float s) {
1360
+ float cycle = iTime * uSpeed * 1.5 + hash12(vec2(k, 8.8));
1361
+ float behind = fract(cycle) * (RUN + abs(laneOf(k, j + 1.0) - laneOf(k, j))) - s;
1362
+ float lit = step(hash12(vec2(k, floor(cycle) - j)), uPulses);
1363
+ return lit * (behind >= 0.0 ? exp(-behind * 0.8) : exp(behind * 30.0));
1364
+ }
1365
+
1366
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1367
+ float px = uLanes / iResolution.y;
1368
+ vec2 p = fragCoord * px;
1369
+ p.y += iTime * uScroll;
1370
+
1371
+ vec2 g = abs(fract(p / 1.5) - 0.5);
1372
+ float grid = 1.0 - smoothstep(0.0, px * 1.5, (0.5 - max(g.x, g.y)) * 1.5);
1373
+ vec3 light = uColorD * 0.12 * uGrid * grid;
1374
+
1375
+ float home = floor(p.x / 3.0);
1376
+ for (int i = -1; i <= 1; i++) {
1377
+ float k = home + float(i);
1378
+ if (hash12(vec2(k, 1.9)) > uDensity) continue;
1379
+ vec2 q = vec2(p.x, p.y + hash12(vec2(k, 4.1)) * RUN);
1380
+ float jq = floor(q.y / RUN);
1381
+ float w = uWidth * 0.5;
1382
+ // The trace's nearest point lights it; each piece's pulse lights only round that piece.
1383
+ float d = 1e5;
1384
+ float pulse = 0.0;
1385
+ for (int b = -1; b <= 1; b++) {
1386
+ float j = jq + float(b);
1387
+ vec4 r = run(q, k, j);
1388
+ d = min(d, min(r.x, r.z));
1389
+ pulse = max(pulse, pulseAt(k, j, r.y) * exp(-r.x / uWidth));
1390
+ if (r.z < 1e4) pulse = max(pulse, pulseAt(k, j, r.w) * exp(-r.z / uWidth));
1391
+ }
1392
+ float core = 1.0 - smoothstep(w - px, w + px, d);
1393
+ float glow = uGlow * (0.5 * exp(-d / uWidth) + 0.2 * exp(-d / (uWidth * 5.0)));
1394
+ vec3 c = traceColor(k);
1395
+ light += c * (core * 0.8 + glow + uGlow * pulse) + mix(c, vec3(1.0), 0.7) * core * pulse * 3.0;
1396
+ }
1397
+ fragColor = vec4(uBackground + (1.0 - exp(-light * 1.4)), 1.0);
1398
+ }`
1399
+ ),
1400
+ presets: [
1401
+ { name: "Mainframe", values: { uBackground: [0, 0.04, 0.02], uColorA: [0.2, 1, 0.45], uColorB: [0.1, 0.75, 0.35], uColorC: [0.6, 1, 0.7], uColorD: [0.1, 0.5, 0.25] } },
1402
+ { name: "Blueprint", values: { uBackground: [0.02, 0.06, 0.16], uColorA: [0.35, 0.75, 1], uColorB: [0.6, 0.9, 1], uColorC: [1, 1, 1], uColorD: [0.25, 0.45, 0.9], uGlow: 0.6, uPulses: 0.3 } },
1403
+ { name: "Dense", values: { uLanes: 26, uDensity: 1, uWidth: 0.12, uPulses: 0.8 } }
1404
+ ]
1405
+ };
1237
1406
 
1238
1407
  // src/catalog/shaders/retro.ts
1239
1408
  var synthwaveGrid = {
@@ -1393,11 +1562,58 @@ var EFFECT_CATEGORIES = [
1393
1562
  "data",
1394
1563
  "icon"
1395
1564
  ];
1565
+ var VECTOR_CONTROL_TYPES = ["float", "int", "bool", "color", "text"];
1566
+ var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
1567
+ function motionStrings(motion) {
1568
+ const out = [];
1569
+ const walk = (v) => {
1570
+ if (typeof v === "string") out.push(v);
1571
+ else if (Array.isArray(v)) v.forEach(walk);
1572
+ else if (v && typeof v === "object") Object.values(v).forEach(walk);
1573
+ };
1574
+ walk(motion);
1575
+ return out;
1576
+ }
1577
+ function vectorControls(motion) {
1578
+ const controls = [];
1579
+ const seen = /* @__PURE__ */ new Set();
1580
+ for (const text of motionStrings(motion)) {
1581
+ for (const [, name, type, attributes] of text.matchAll(PLACEHOLDER)) {
1582
+ if (type === void 0 || seen.has(name) || !VECTOR_CONTROL_TYPES.includes(type)) continue;
1583
+ seen.add(name);
1584
+ const colour = type === "color";
1585
+ controls.push(controlFromDeclaration(name, colour ? "vec3" : type, `${attributes}${colour ? " color" : ""}`, controls.length + 1));
1586
+ }
1587
+ }
1588
+ return controls;
1589
+ }
1590
+ function vectorEffectControls(effect) {
1591
+ const declared = vectorControls(effect.motion);
1592
+ const names = new Set(declared.map((c) => c.name));
1593
+ const listed = (effect.controls ?? []).filter((c) => !names.has(c.name));
1594
+ return [...declared, ...listed.map((c, i) => vectorControlDef(c, declared.length + i + 1))];
1595
+ }
1596
+ function vectorControlIssues(motion) {
1597
+ const issues = [];
1598
+ const declarations = /* @__PURE__ */ new Map();
1599
+ for (const text of motionStrings(motion)) {
1600
+ for (const [whole, name, type] of text.matchAll(PLACEHOLDER)) {
1601
+ if (type === void 0) continue;
1602
+ if (!VECTOR_CONTROL_TYPES.includes(type))
1603
+ issues.push(`${whole}: a control's type is one of ${VECTOR_CONTROL_TYPES.join(", ")}`);
1604
+ else if (declarations.has(name) && declarations.get(name) !== whole)
1605
+ issues.push(`"${name}" is declared twice; declare it once and write {{${name}}} everywhere else`);
1606
+ else declarations.set(name, whole);
1607
+ }
1608
+ }
1609
+ return issues;
1610
+ }
1396
1611
  function written(control, value) {
1397
1612
  if (control.color && Array.isArray(value)) return rgbToHex(value);
1398
1613
  if (Array.isArray(value)) return value.join(" ");
1399
1614
  return value;
1400
1615
  }
1616
+ var escapeMarkup = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1401
1617
  function vectorControlDef(control, line = 0) {
1402
1618
  const min = control.min ?? 0;
1403
1619
  return {
@@ -1415,18 +1631,30 @@ function vectorControlDef(control, line = 0) {
1415
1631
  function renderVectorEffect(effect, values = {}) {
1416
1632
  const filled = {};
1417
1633
  const placeholders = /* @__PURE__ */ new Map();
1418
- for (const control of effect.controls) {
1419
- filled[control.name] = values[control.name] ?? control.default;
1634
+ for (const control of vectorEffectControls(effect)) {
1635
+ const given = values[control.name];
1636
+ const check = given === void 0 ? void 0 : checkControlValue(control, given);
1637
+ filled[control.name] = check?.ok ? check.value : control.default;
1420
1638
  placeholders.set(control.name, written(control, filled[control.name]));
1421
1639
  }
1422
1640
  for (const [name, value] of Object.entries(effect.derived?.(filled) ?? {})) placeholders.set(name, value);
1423
- let json = JSON.stringify(effect.motion);
1424
- for (const [key, value] of placeholders) {
1425
- const name = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1426
- json = json.replace(new RegExp(`"\\{\\{${name}\\}\\}"`, "g"), JSON.stringify(value));
1427
- json = json.replace(new RegExp(`\\{\\{${name}\\}\\}`, "g"), String(value).replace(/["\\]/g, "\\$&"));
1428
- }
1429
- return JSON.parse(json);
1641
+ const fill = (text, markup) => {
1642
+ const whole = /^\{\{\s*([A-Za-z_]\w*)\s*(?::[^}]*)?\}\}$/.exec(text);
1643
+ if (!markup && whole && placeholders.has(whole[1])) return placeholders.get(whole[1]);
1644
+ return text.replace(PLACEHOLDER, (match, name) => {
1645
+ if (!placeholders.has(name)) return match;
1646
+ const value = String(placeholders.get(name));
1647
+ return markup ? escapeMarkup(value) : value;
1648
+ });
1649
+ };
1650
+ const walk = (v) => {
1651
+ if (typeof v === "string") return fill(v, false);
1652
+ if (Array.isArray(v)) return v.map(walk);
1653
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
1654
+ return v;
1655
+ };
1656
+ const { svg, ...rest } = effect.motion;
1657
+ return { svg: fill(svg.replace(/<!--[\s\S]*?-->/g, ""), true), ...walk(rest) };
1430
1658
  }
1431
1659
  function colorControl(name, hex, label) {
1432
1660
  return { name, type: "vec3", color: true, default: hexToRgb(hex) ?? [1, 1, 1], label: label ?? name };
@@ -1444,9 +1672,8 @@ var spinner = {
1444
1672
  category: "loader",
1445
1673
  tags: ["loader", "ui", "minimal"],
1446
1674
  hold: 0,
1447
- controls: [colorControl("accent", "#6366F1", "Arc"), colorControl("track", "#E5E7EB", "Track"), numberControl("width", 8, 2, 20, "Stroke width", 1)],
1448
1675
  motion: {
1449
- svg: `<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="38" fill="none" stroke="{{track}}" stroke-width="{{width}}"/><g id="spin" transform="rotate(0 50 50)"><circle id="arc" cx="50" cy="50" r="38" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" pathLength="1" stroke-dasharray="0.25 1"/></g></svg>`,
1676
+ svg: `<svg viewBox="0 0 100 100"><!-- {{accent: color default=#6366F1 label='Arc'}} {{track: color default=#E5E7EB label='Track'}} {{width: float min=2 max=20 default=8 step=1 label='Stroke width'}} --><circle cx="50" cy="50" r="38" fill="none" stroke="{{track}}" stroke-width="{{width}}"/><g id="spin" transform="rotate(0 50 50)"><circle id="arc" cx="50" cy="50" r="38" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" pathLength="1" stroke-dasharray="0.25 1"/></g></svg>`,
1450
1677
  animate: [
1451
1678
  { id: "spin", attrs: { transform: ["rotate(0 50 50)", "rotate(720 50 50)"] }, ms: 1600, ease: "linear" },
1452
1679
  { id: "arc", attrs: { "stroke-dasharray": ["0.08 1", "0.6 1"] }, ms: 800, ease: "in-out" },
@@ -1462,9 +1689,8 @@ var bouncingDots = {
1462
1689
  category: "loader",
1463
1690
  tags: ["loader", "chat", "typing", "ui"],
1464
1691
  hold: 0,
1465
- controls: [colorControl("accent", "#0EA5E9", "Dots"), numberControl("height", 18, 4, 30, "Bounce height", 1)],
1466
1692
  motion: {
1467
- svg: `<svg viewBox="0 0 120 60"><circle id="a" cx="30" cy="40" r="9" fill="{{accent}}"/><circle id="b" cx="60" cy="40" r="9" fill="{{accent}}"/><circle id="c" cx="90" cy="40" r="9" fill="{{accent}}"/></svg>`,
1693
+ svg: `<svg viewBox="0 0 120 60"><!-- {{accent: color default=#0EA5E9 label='Dots'}} {{height: float min=4 max=30 default=18 step=1 label='Bounce height'}} --><circle id="a" cx="30" cy="40" r="9" fill="{{accent}}"/><circle id="b" cx="60" cy="40" r="9" fill="{{accent}}"/><circle id="c" cx="90" cy="40" r="9" fill="{{accent}}"/></svg>`,
1468
1694
  animate: ["a", "b", "c"].flatMap((id, i) => [
1469
1695
  { id, attrs: { transform: ["translate(0 0)", "translate(0 -{{height}})"] }, delay: i * 150, ms: 250, ease: "out" },
1470
1696
  { id, attrs: { transform: ["translate(0 -{{height}})", "translate(0 0)"] }, delay: i * 150 + 250, ms: 250, ease: "in-out" },
@@ -1480,15 +1706,9 @@ var progressRing = {
1480
1706
  description: "A ring filling up to a percentage while the number counts with it.",
1481
1707
  category: "data",
1482
1708
  tags: ["progress", "kpi", "dashboard", "loader"],
1483
- controls: [
1484
- colorControl("accent", "#22C55E", "Ring"),
1485
- colorControl("track", "#E5E7EB", "Track"),
1486
- colorControl("text", "#111827", "Text"),
1487
- numberControl("value", 72, 0, 100, "Percent", 1)
1488
- ],
1489
1709
  derived: (v) => ({ fraction: Math.min(1, Math.max(0, Number(v.value) / 100)) }),
1490
1710
  motion: {
1491
- svg: `<svg viewBox="0 0 120 120" font-family="system-ui, sans-serif"><circle cx="60" cy="60" r="48" fill="none" stroke="{{track}}" stroke-width="10"/><circle id="ring" cx="60" cy="60" r="48" fill="none" stroke="{{accent}}" stroke-width="10" stroke-linecap="round" transform="rotate(-90 60 60)"/><text id="label" x="60" y="68" text-anchor="middle" font-size="24" font-weight="700" fill="{{text}}">0%</text></svg>`,
1711
+ svg: `<svg viewBox="0 0 120 120" font-family="system-ui, sans-serif"><!-- {{accent: color default=#22C55E label='Ring'}} {{track: color default=#E5E7EB label='Track'}} {{text: color default=#111827 label='Text'}} {{value: float min=0 max=100 default=72 step=1 label='Percent'}} --><circle cx="60" cy="60" r="48" fill="none" stroke="{{track}}" stroke-width="10"/><circle id="ring" cx="60" cy="60" r="48" fill="none" stroke="{{accent}}" stroke-width="10" stroke-linecap="round" transform="rotate(-90 60 60)"/><text id="label" x="60" y="68" text-anchor="middle" font-size="24" font-weight="700" fill="{{text}}">0%</text></svg>`,
1492
1712
  animate: [
1493
1713
  { id: "ring", draw: [0, "{{fraction}}"], ms: 1600, ease: "out" },
1494
1714
  { id: "label", count: [0, "{{value}}"], format: "{n}%", ms: 1600, ease: "out" }
@@ -1503,9 +1723,8 @@ var pulsePing = {
1503
1723
  category: "loader",
1504
1724
  tags: ["live", "notification", "location", "status"],
1505
1725
  hold: 0,
1506
- controls: [colorControl("accent", "#EF4444", "Colour")],
1507
1726
  motion: {
1508
- svg: `<svg viewBox="0 0 100 100"><circle id="r1" cx="50" cy="50" r="8" fill="none" stroke="{{accent}}" stroke-width="3"/><circle id="r2" cx="50" cy="50" r="8" fill="none" stroke="{{accent}}" stroke-width="3"/><circle cx="50" cy="50" r="9" fill="{{accent}}"/></svg>`,
1727
+ svg: `<svg viewBox="0 0 100 100"><!-- {{accent: color default=#EF4444 label='Colour'}} --><circle id="r1" cx="50" cy="50" r="8" fill="none" stroke="{{accent}}" stroke-width="3"/><circle id="r2" cx="50" cy="50" r="8" fill="none" stroke="{{accent}}" stroke-width="3"/><circle cx="50" cy="50" r="9" fill="{{accent}}"/></svg>`,
1509
1728
  animate: [
1510
1729
  { id: "r1", attrs: { r: [8, 44], opacity: [0.9, 0], "stroke-width": [4, 1] }, ms: 1400, ease: "out" },
1511
1730
  { id: "r2", attrs: { r: [8, 44], opacity: [0.9, 0], "stroke-width": [4, 1] }, delay: 700, ms: 1400, ease: "out" }
@@ -1520,9 +1739,8 @@ var successCheck = {
1520
1739
  category: "icon",
1521
1740
  tags: ["success", "confirmation", "ui", "feedback"],
1522
1741
  hold: 1200,
1523
- controls: [colorControl("accent", "#16A34A", "Colour"), numberControl("width", 6, 2, 12, "Stroke width", 1)],
1524
1742
  motion: {
1525
- svg: `<svg viewBox="0 0 100 100"><g id="badge" transform="translate(50 50) scale(1) translate(-50 -50)"><circle id="circle" cx="50" cy="50" r="40" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" transform="rotate(-90 50 50)"/><path id="tick" d="M31 52 L44 64 L70 37" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"/></g></svg>`,
1743
+ svg: `<svg viewBox="0 0 100 100"><!-- {{accent: color default=#16A34A label='Colour'}} {{width: float min=2 max=12 default=6 step=1 label='Stroke width'}} --><g id="badge" transform="translate(50 50) scale(1) translate(-50 -50)"><circle id="circle" cx="50" cy="50" r="40" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" transform="rotate(-90 50 50)"/><path id="tick" d="M31 52 L44 64 L70 37" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"/></g></svg>`,
1526
1744
  animate: [
1527
1745
  { id: "circle", draw: true, ms: 600, ease: "in-out" },
1528
1746
  { id: "tick", draw: true, delay: 500, ms: 400, ease: "out" },
@@ -1539,9 +1757,8 @@ var errorCross = {
1539
1757
  category: "icon",
1540
1758
  tags: ["error", "failure", "ui", "feedback"],
1541
1759
  hold: 1200,
1542
- controls: [colorControl("accent", "#DC2626", "Colour"), numberControl("width", 6, 2, 12, "Stroke width", 1)],
1543
1760
  motion: {
1544
- svg: `<svg viewBox="0 0 100 100"><g id="badge" transform="translate(0 0)"><circle id="circle" cx="50" cy="50" r="40" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" transform="rotate(-90 50 50)"/><path id="a" d="M36 36 L64 64" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round"/><path id="b" d="M64 36 L36 64" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round"/></g></svg>`,
1761
+ svg: `<svg viewBox="0 0 100 100"><!-- {{accent: color default=#DC2626 label='Colour'}} {{width: float min=2 max=12 default=6 step=1 label='Stroke width'}} --><g id="badge" transform="translate(0 0)"><circle id="circle" cx="50" cy="50" r="40" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round" transform="rotate(-90 50 50)"/><path id="a" d="M36 36 L64 64" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round"/><path id="b" d="M64 36 L36 64" fill="none" stroke="{{accent}}" stroke-width="{{width}}" stroke-linecap="round"/></g></svg>`,
1545
1762
  animate: [
1546
1763
  { id: "circle", draw: true, ms: 600, ease: "in-out" },
1547
1764
  { id: "a", draw: true, delay: 500, ms: 250, ease: "out" },
@@ -1560,9 +1777,8 @@ var heartbeat = {
1560
1777
  category: "icon",
1561
1778
  tags: ["health", "like", "love", "pulse"],
1562
1779
  hold: 300,
1563
- controls: [colorControl("accent", "#F43F5E", "Heart"), colorControl("line", "#94A3B8", "Line")],
1564
1780
  motion: {
1565
- svg: `<svg viewBox="0 0 200 120"><g id="heart" transform="translate(100 46) scale(1)"><path d="M0 22 C-30 0 -30 -26 -10 -26 C-3 -26 0 -20 0 -16 C0 -20 3 -26 10 -26 C30 -26 30 0 0 22Z" fill="{{accent}}"/></g><path id="ecg" d="M10 100 H70 L80 86 L90 112 L100 76 L110 104 L118 100 H190" fill="none" stroke="{{line}}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/></svg>`,
1781
+ svg: `<svg viewBox="0 0 200 120"><!-- {{accent: color default=#F43F5E label='Heart'}} {{line: color default=#94A3B8 label='Line'}} --><g id="heart" transform="translate(100 46) scale(1)"><path d="M0 22 C-30 0 -30 -26 -10 -26 C-3 -26 0 -20 0 -16 C0 -20 3 -26 10 -26 C30 -26 30 0 0 22Z" fill="{{accent}}"/></g><path id="ecg" d="M10 100 H70 L80 86 L90 112 L100 76 L110 104 L118 100 H190" fill="none" stroke="{{line}}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/></svg>`,
1566
1782
  animate: [
1567
1783
  { id: "heart", attrs: { transform: ["translate(100 46) scale(1)", "translate(100 46) scale(1.18)"] }, ms: 120, ease: "out" },
1568
1784
  { id: "heart", attrs: { transform: ["translate(100 46) scale(1.18)", "translate(100 46) scale(1)"] }, delay: 120, ms: 160, ease: "in-out" },
@@ -1580,14 +1796,6 @@ var barChart = {
1580
1796
  category: "data",
1581
1797
  tags: ["chart", "dashboard", "report", "explainer"],
1582
1798
  hold: 1500,
1583
- controls: [
1584
- colorControl("accent", "#6366F1", "Bars"),
1585
- colorControl("text", "#334155", "Text"),
1586
- numberControl("a", 40, 0, 100, "Bar 1", 1),
1587
- numberControl("b", 65, 0, 100, "Bar 2", 1),
1588
- numberControl("c", 50, 0, 100, "Bar 3", 1),
1589
- numberControl("d", 90, 0, 100, "Bar 4", 1)
1590
- ],
1591
1799
  derived: (v) => Object.fromEntries(
1592
1800
  ["a", "b", "c", "d"].flatMap((k) => {
1593
1801
  const h = Math.min(100, Math.max(0, Number(v[k]))) * 1.1;
@@ -1598,7 +1806,7 @@ var barChart = {
1598
1806
  })
1599
1807
  ),
1600
1808
  motion: {
1601
- svg: `<svg viewBox="0 0 240 160" font-family="system-ui, sans-serif"><line x1="20" y1="140" x2="220" y2="140" stroke="{{text}}" stroke-opacity="0.3" stroke-width="2"/>${["a", "b", "c", "d"].map(
1809
+ svg: `<svg viewBox="0 0 240 160" font-family="system-ui, sans-serif"><!-- {{accent: color default=#6366F1 label='Bars'}} {{text: color default=#334155 label='Text'}} {{a: float min=0 max=100 default=40 step=1 label='Bar 1'}} {{b: float min=0 max=100 default=65 step=1 label='Bar 2'}} {{c: float min=0 max=100 default=50 step=1 label='Bar 3'}} {{d: float min=0 max=100 default=90 step=1 label='Bar 4'}} --><line x1="20" y1="140" x2="220" y2="140" stroke="{{text}}" stroke-opacity="0.3" stroke-width="2"/>${["a", "b", "c", "d"].map(
1602
1810
  (k, i) => `<g id="bar-${k}" transform="translate(${40 + i * 48} 140) scale(1 0)"><rect x="0" y="-1" width="32" height="1" rx="0.02" fill="{{accent}}"/></g><text id="label-${k}" x="${56 + i * 48}" y="132" text-anchor="middle" font-size="12" fill="{{text}}" opacity="0">0</text>`
1603
1811
  ).join("")}</svg>`,
1604
1812
  animate: ["a", "b", "c", "d"].flatMap((k, i) => [
@@ -1616,9 +1824,8 @@ var lineChart = {
1616
1824
  category: "data",
1617
1825
  tags: ["chart", "trend", "growth", "dashboard"],
1618
1826
  hold: 1500,
1619
- controls: [colorControl("accent", "#0EA5E9", "Line"), colorControl("grid", "#CBD5E1", "Grid")],
1620
1827
  motion: {
1621
- svg: `<svg viewBox="0 0 240 140"><defs><linearGradient id="fill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.35"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><g stroke="{{grid}}" stroke-width="1"><line x1="20" y1="30" x2="220" y2="30"/><line x1="20" y1="70" x2="220" y2="70"/><line x1="20" y1="110" x2="220" y2="110"/></g><path id="area" d="M20 110 L60 92 L100 98 L140 64 L180 70 L220 30 L220 120 L20 120Z" fill="url(#fill)" opacity="0"/><path id="trend" d="M20 110 L60 92 L100 98 L140 64 L180 70 L220 30" fill="none" stroke="{{accent}}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/><circle id="marker" r="5" fill="#fff" stroke="{{accent}}" stroke-width="3"/></svg>`,
1828
+ svg: `<svg viewBox="0 0 240 140"><!-- {{accent: color default=#0EA5E9 label='Line'}} {{grid: color default=#CBD5E1 label='Grid'}} --><defs><linearGradient id="fill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.35"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><g stroke="{{grid}}" stroke-width="1"><line x1="20" y1="30" x2="220" y2="30"/><line x1="20" y1="70" x2="220" y2="70"/><line x1="20" y1="110" x2="220" y2="110"/></g><path id="area" d="M20 110 L60 92 L100 98 L140 64 L180 70 L220 30 L220 120 L20 120Z" fill="url(#fill)" opacity="0"/><path id="trend" d="M20 110 L60 92 L100 98 L140 64 L180 70 L220 30" fill="none" stroke="{{accent}}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/><circle id="marker" r="5" fill="#fff" stroke="{{accent}}" stroke-width="3"/></svg>`,
1622
1829
  animate: [
1623
1830
  { id: "trend", draw: true, ms: 1400, ease: "in-out" },
1624
1831
  { id: "marker", along: "trend", ms: 1400, ease: "in-out" },
@@ -1634,13 +1841,8 @@ var statCounter = {
1634
1841
  category: "data",
1635
1842
  tags: ["kpi", "number", "headline", "explainer"],
1636
1843
  hold: 1500,
1637
- controls: [
1638
- colorControl("accent", "#F59E0B", "Accent"),
1639
- colorControl("text", "#0F172A", "Text"),
1640
- numberControl("value", 1250, 0, 1e5, "Value", 1)
1641
- ],
1642
1844
  motion: {
1643
- svg: `<svg viewBox="0 0 240 120" font-family="system-ui, sans-serif"><text id="number" x="120" y="62" text-anchor="middle" font-size="44" font-weight="800" fill="{{text}}">0</text><path id="underline" d="M60 78 H180" stroke="{{accent}}" stroke-width="6" stroke-linecap="round"/><text id="caption" x="120" y="104" text-anchor="middle" font-size="13" fill="{{text}}" opacity="0">happy customers</text></svg>`,
1845
+ svg: `<svg viewBox="0 0 240 120" font-family="system-ui, sans-serif"><!-- {{accent: color default=#F59E0B label='Accent'}} {{text: color default=#0F172A label='Text'}} {{value: float min=0 max=100000 default=1250 step=1 label='Value'}} {{caption: text default='happy customers' label='Caption'}} --><text id="number" x="120" y="62" text-anchor="middle" font-size="44" font-weight="800" fill="{{text}}">0</text><path id="underline" d="M60 78 H180" stroke="{{accent}}" stroke-width="6" stroke-linecap="round"/><text id="caption" x="120" y="104" text-anchor="middle" font-size="13" fill="{{text}}" opacity="0">{{caption}}</text></svg>`,
1644
1846
  animate: [
1645
1847
  { id: "number", count: [0, "{{value}}"], ms: 1500, ease: "out" },
1646
1848
  { id: "underline", draw: true, delay: 300, ms: 900, ease: "in-out" },
@@ -1656,9 +1858,8 @@ var route = {
1656
1858
  category: "data",
1657
1859
  tags: ["map", "travel", "delivery", "journey"],
1658
1860
  hold: 1200,
1659
- controls: [colorControl("accent", "#8B5CF6", "Route"), colorControl("pin", "#0F172A", "Pins")],
1660
1861
  motion: {
1661
- svg: `<svg viewBox="0 0 240 150"><path id="path" d="M30 120 C60 40 120 140 150 70 S210 30 210 30" fill="none" stroke="{{accent}}" stroke-width="4" stroke-linecap="round"/><circle cx="30" cy="120" r="7" fill="{{pin}}"/><g id="end" opacity="0"><circle cx="210" cy="30" r="9" fill="{{pin}}"/><circle cx="210" cy="30" r="3.5" fill="#fff"/></g><g id="traveller"><circle r="7" fill="{{accent}}"/><circle r="3" fill="#fff"/></g></svg>`,
1862
+ svg: `<svg viewBox="0 0 240 150"><!-- {{accent: color default=#8B5CF6 label='Route'}} {{pin: color default=#0F172A label='Pins'}} --><path id="path" d="M30 120 C60 40 120 140 150 70 S210 30 210 30" fill="none" stroke="{{accent}}" stroke-width="4" stroke-linecap="round"/><circle cx="30" cy="120" r="7" fill="{{pin}}"/><g id="end" opacity="0"><circle cx="210" cy="30" r="9" fill="{{pin}}"/><circle cx="210" cy="30" r="3.5" fill="#fff"/></g><g id="traveller"><circle r="7" fill="{{accent}}"/><circle r="3" fill="#fff"/></g></svg>`,
1662
1863
  animate: [
1663
1864
  { id: "path", draw: true, ms: 1800, ease: "in-out" },
1664
1865
  { id: "traveller", along: "path", ms: 1800, ease: "in-out" },
@@ -1674,7 +1875,6 @@ var blobMorph = {
1674
1875
  category: "organic",
1675
1876
  tags: ["shape", "background", "playful", "brand"],
1676
1877
  hold: 0,
1677
- controls: [colorControl("colorA", "#F472B6", "Colour A"), colorControl("colorB", "#818CF8", "Colour B")],
1678
1878
  motion: (() => {
1679
1879
  const shapes = [
1680
1880
  "M100 30 C140 30 170 60 170 100 C170 140 140 170 100 170 C60 170 30 140 30 100 C30 60 60 30 100 30Z",
@@ -1683,7 +1883,7 @@ var blobMorph = {
1683
1883
  ];
1684
1884
  const step = 1200;
1685
1885
  return {
1686
- svg: `<svg viewBox="0 0 200 200"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="{{colorA}}"/><stop offset="1" stop-color="{{colorB}}"/></linearGradient></defs><path id="blob" d="${shapes[0]}" fill="url(#g)"/></svg>`,
1886
+ svg: `<svg viewBox="0 0 200 200"><!-- {{colorA: color default=#F472B6 label='Colour A'}} {{colorB: color default=#818CF8 label='Colour B'}} --><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="{{colorA}}"/><stop offset="1" stop-color="{{colorB}}"/></linearGradient></defs><path id="blob" d="${shapes[0]}" fill="url(#g)"/></svg>`,
1687
1887
  animate: shapes.map((d, i) => ({
1688
1888
  id: "blob",
1689
1889
  attrs: { d: [d, shapes[(i + 1) % shapes.length]] },
@@ -1702,9 +1902,8 @@ var signature = {
1702
1902
  category: "icon",
1703
1903
  tags: ["handwriting", "draw-on", "personal", "reveal"],
1704
1904
  hold: 1500,
1705
- controls: [colorControl("ink", "#1E293B", "Ink"), numberControl("width", 3, 1, 8, "Pen width", 0.5)],
1706
1905
  motion: {
1707
- svg: `<svg viewBox="0 0 260 120"><path id="sig" d="M20 80 C40 20 60 20 55 70 C52 95 70 95 85 60 C95 40 100 75 110 70 C120 65 125 40 135 55 C145 70 150 75 165 50 C175 35 185 60 190 65 C200 75 215 40 240 45" fill="none" stroke="{{ink}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"/><path id="dash" d="M70 100 Q140 92 220 98" fill="none" stroke="{{ink}}" stroke-width="{{width}}" stroke-linecap="round"/></svg>`,
1906
+ svg: `<svg viewBox="0 0 260 120"><!-- {{ink: color default=#1E293B label='Ink'}} {{width: float min=1 max=8 default=3 step=0.5 label='Pen width'}} --><path id="sig" d="M20 80 C40 20 60 20 55 70 C52 95 70 95 85 60 C95 40 100 75 110 70 C120 65 125 40 135 55 C145 70 150 75 165 50 C175 35 185 60 190 65 C200 75 215 40 240 45" fill="none" stroke="{{ink}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"/><path id="dash" d="M70 100 Q140 92 220 98" fill="none" stroke="{{ink}}" stroke-width="{{width}}" stroke-linecap="round"/></svg>`,
1708
1907
  animate: [
1709
1908
  { id: "sig", draw: true, ms: 1800, ease: "in-out" },
1710
1909
  { id: "dash", draw: true, delay: 1700, ms: 400, ease: "out" }
@@ -1719,7 +1918,6 @@ var equalizer = {
1719
1918
  category: "loader",
1720
1919
  tags: ["music", "audio", "voice", "now-playing"],
1721
1920
  hold: 0,
1722
- controls: [colorControl("accent", "#10B981", "Bars")],
1723
1921
  motion: (() => {
1724
1922
  const heights = [
1725
1923
  [0.3, 0.9, 0.5, 0.3],
@@ -1730,7 +1928,7 @@ var equalizer = {
1730
1928
  ];
1731
1929
  const beat = 250;
1732
1930
  return {
1733
- svg: `<svg viewBox="0 0 100 100">${heights.map((_, i) => `<g id="bar${i}" transform="translate(${14 + i * 16} 85) scale(1 ${heights[i][0]})"><rect x="0" y="-70" width="10" height="70" rx="5" fill="{{accent}}"/></g>`).join("")}</svg>`,
1931
+ svg: `<svg viewBox="0 0 100 100"><!-- {{accent: color default=#10B981 label='Bars'}} -->${heights.map((_, i) => `<g id="bar${i}" transform="translate(${14 + i * 16} 85) scale(1 ${heights[i][0]})"><rect x="0" y="-70" width="10" height="70" rx="5" fill="{{accent}}"/></g>`).join("")}</svg>`,
1734
1932
  animate: heights.flatMap(
1735
1933
  (h, i) => [0, 1, 2].map((k) => ({
1736
1934
  id: `bar${i}`,
@@ -1751,7 +1949,6 @@ var confetti = {
1751
1949
  category: "icon",
1752
1950
  tags: ["celebration", "success", "party", "reward"],
1753
1951
  hold: 400,
1754
- controls: [colorControl("a", "#F43F5E", "Colour A"), colorControl("b", "#FACC15", "Colour B"), colorControl("c", "#22D3EE", "Colour C")],
1755
1952
  motion: (() => {
1756
1953
  const pieces = Array.from({ length: 18 }, (_, i) => {
1757
1954
  const angle = i / 18 * Math.PI * 2 + i % 3 * 0.2;
@@ -1762,7 +1959,7 @@ var confetti = {
1762
1959
  return { i, x, y, spin, fill: ["{{a}}", "{{b}}", "{{c}}"][i % 3] };
1763
1960
  });
1764
1961
  return {
1765
- svg: `<svg viewBox="0 0 200 200">${pieces.map((p) => `<rect id="p${p.i}" x="-4" y="-2" width="8" height="4" rx="1" fill="${p.fill}" transform="translate(100 110) rotate(0)" opacity="0"/>`).join("")}</svg>`,
1962
+ svg: `<svg viewBox="0 0 200 200"><!-- {{a: color default=#F43F5E label='Colour A'}} {{b: color default=#FACC15 label='Colour B'}} {{c: color default=#22D3EE label='Colour C'}} -->${pieces.map((p) => `<rect id="p${p.i}" x="-4" y="-2" width="8" height="4" rx="1" fill="${p.fill}" transform="translate(100 110) rotate(0)" opacity="0"/>`).join("")}</svg>`,
1766
1963
  animate: pieces.flatMap((p) => [
1767
1964
  { id: `p${p.i}`, attrs: { opacity: [0, 1] }, ms: 50 },
1768
1965
  {
@@ -1785,6 +1982,152 @@ var confetti = {
1785
1982
  };
1786
1983
  })()
1787
1984
  };
1985
+ var NODES = {
1986
+ s: [40, 120],
1987
+ a: [120, 60],
1988
+ b: [120, 180],
1989
+ c: [280, 50],
1990
+ d: [290, 130],
1991
+ e: [280, 200],
1992
+ t: [360, 120]
1993
+ };
1994
+ var HUB = [200, 120];
1995
+ var link = ([x1, y1], [x2, y2]) => {
1996
+ const mid = (x1 + x2) / 2;
1997
+ return `M${x1} ${y1} C${mid} ${y1} ${mid} ${y2} ${x2} ${y2}`;
1998
+ };
1999
+ var LINKS = [
2000
+ { from: NODES.s, to: NODES.a, column: 0 },
2001
+ { from: NODES.s, to: NODES.b, column: 0 },
2002
+ { from: NODES.a, to: HUB, column: 1 },
2003
+ { from: NODES.b, to: HUB, column: 1 },
2004
+ { from: HUB, to: NODES.c, column: 2 },
2005
+ { from: HUB, to: NODES.d, column: 2 },
2006
+ { from: HUB, to: NODES.e, column: 2 },
2007
+ { from: NODES.c, to: NODES.t, column: 3 },
2008
+ { from: NODES.d, to: NODES.t, column: 3 },
2009
+ { from: NODES.e, to: NODES.t, column: 3 }
2010
+ ];
2011
+ var CRITICAL = [NODES.s, NODES.b, HUB, NODES.c, NODES.t].map((p, i, all) => i === 0 ? "" : link(all[i - 1], p).replace(/^M[\d.]+ [\d.]+ /, i === 1 ? `M${all[0][0]} ${all[0][1]} ` : "")).filter(Boolean).join(" ");
2012
+ var NODE_COLUMN = { s: 0, a: 1, b: 1, c: 3, d: 3, e: 3, t: 4 };
2013
+ var COLUMN_MS = 280;
2014
+ var PATH_START = 1500;
2015
+ var PATH_MS = 1800;
2016
+ var flowNetwork = {
2017
+ kind: "vector",
2018
+ id: "flow-network",
2019
+ name: "Flow network",
2020
+ description: "A node graph linking up, then a glowing critical path racing through it to a pulsing hub.",
2021
+ category: "data",
2022
+ tags: ["network", "graph", "dag", "workflow", "tech", "explainer"],
2023
+ hold: 1200,
2024
+ presets: [
2025
+ { name: "Light", values: { background: [0.93, 0.95, 0.96], link: [0.37, 0.72, 0.69], path: [0.96, 0.62, 0.04], ink: [0.2, 0.25, 0.33] } },
2026
+ { name: "Neon", values: { background: [0.04, 0.02, 0.1], link: [0.13, 0.83, 0.93], path: [0.96, 0.45, 0.71], ink: [0.85, 0.85, 1] } }
2027
+ ],
2028
+ motion: {
2029
+ svg: `<svg viewBox="0 0 400 240" font-family="system-ui, sans-serif"><!-- {{label: text default='Critical path' label='Hub label'}} {{background: color default=#0B1220 label='Background'}} {{link: color default=#4FD1C5 label='Links'}} {{path: color default=#F5A524 label='Critical path'}} {{ink: color default=#CBD5E1 label='Label'}} {{glow: float min=0 max=6 default=2.5 step=0.5 label='Glow'}} --><defs><filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="{{glow}}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect width="400" height="240" fill="{{background}}"/>${LINKS.map(
2030
+ (l, i) => `<path id="l${i}" d="${link(l.from, l.to)}" fill="none" stroke="{{link}}" stroke-width="1.6" stroke-opacity="0.75" filter="url(#glow)"/>`
2031
+ ).join("")}<path id="crit" d="${CRITICAL}" fill="none" stroke="{{path}}" stroke-width="3" stroke-linecap="round" filter="url(#glow)"/>${[0, 1].map((i) => `<circle id="ring${i}" cx="${HUB[0]}" cy="${HUB[1]}" r="10" fill="none" stroke="{{link}}" stroke-width="2" opacity="0"/>`).join("")}${Object.entries(NODES).map(([id, [x, y]]) => `<circle id="n${id}" cx="${x}" cy="${y}" r="6" fill="{{background}}" stroke="{{link}}" stroke-width="2.5" filter="url(#glow)" opacity="0"/>`).join("")}<g id="hub" opacity="0"><circle cx="${HUB[0]}" cy="${HUB[1]}" r="13" fill="{{link}}" fill-opacity="0.25" stroke="{{link}}" stroke-width="2" filter="url(#glow)"/><circle cx="${HUB[0]}" cy="${HUB[1]}" r="6" fill="{{link}}"/></g><text id="caption" x="${HUB[0]}" y="${HUB[1] + 34}" text-anchor="middle" font-size="12" font-weight="600" fill="{{ink}}" opacity="0">{{label}}</text>${[0, 1, 2].map((i) => `<circle id="pkt${i}" r="2.5" fill="{{link}}" filter="url(#glow)" opacity="0"/>`).join("")}<circle id="comet" r="5" fill="#FFFFFF" stroke="{{path}}" stroke-width="2" filter="url(#glow)" opacity="0"/></svg>`,
2032
+ animate: [
2033
+ ...LINKS.map((l, i) => ({ id: `l${i}`, draw: true, delay: l.column * COLUMN_MS, ms: 500, ease: "out" })),
2034
+ ...Object.keys(NODES).map((id) => ({ id: `n${id}`, attrs: { opacity: [0, 1] }, delay: Math.max(0, NODE_COLUMN[id] * COLUMN_MS - 120), ms: 250 })),
2035
+ { id: "hub", attrs: { opacity: [0, 1] }, delay: 2 * COLUMN_MS - 120, ms: 300 },
2036
+ { id: "caption", attrs: { opacity: [0, 1], y: [HUB[1] + 40, HUB[1] + 34] }, delay: 2 * COLUMN_MS, ms: 400, ease: "out" },
2037
+ // Data packets drift along a few links while the path waits.
2038
+ ...[3, 5, 8].map((link2, i) => [
2039
+ { id: `pkt${i}`, attrs: { opacity: [0, 1] }, delay: 1e3 + i * 150, ms: 150 },
2040
+ { id: `pkt${i}`, along: `l${link2}`, delay: 1e3 + i * 150, ms: 1100, ease: "in-out" },
2041
+ { id: `pkt${i}`, attrs: { opacity: [1, 0] }, delay: 2e3 + i * 150, ms: 200 }
2042
+ ]).flat(),
2043
+ { id: "crit", draw: true, delay: PATH_START, ms: PATH_MS, ease: "in-out" },
2044
+ { id: "comet", attrs: { opacity: [0, 1] }, delay: PATH_START, ms: 150 },
2045
+ { id: "comet", along: "crit", delay: PATH_START, ms: PATH_MS, ease: "in-out" },
2046
+ { id: "comet", attrs: { opacity: [1, 0] }, delay: PATH_START + PATH_MS, ms: 300 },
2047
+ // Rings pulse out from the hub as the comet passes it, halfway along.
2048
+ ...[0, 1].map((i) => ({
2049
+ id: `ring${i}`,
2050
+ attrs: { r: [10, 52], opacity: [0.9, 0], "stroke-width": [3, 0.5] },
2051
+ delay: PATH_START + PATH_MS / 2 + i * 350,
2052
+ ms: 1200,
2053
+ ease: "out"
2054
+ }))
2055
+ ]
2056
+ }
2057
+ };
2058
+ var DRIP_TOP = 106;
2059
+ var DRIPS = [
2060
+ { at: 0.067, width: 11, length: 34, delay: 0, ms: 1500 },
2061
+ { at: 0.18, width: 8, length: 18, delay: 260, ms: 1100 },
2062
+ { at: 0.3, width: 13, length: 50, delay: 120, ms: 1800 },
2063
+ { at: 0.453, width: 9, length: 22, delay: 380, ms: 1200 },
2064
+ { at: 0.587, width: 10, length: 28, delay: 200, ms: 1400 },
2065
+ { at: 0.693, width: 12, length: 42, delay: 60, ms: 1700 },
2066
+ { at: 0.793, width: 11, length: 32, delay: 300, ms: 1500 }
2067
+ ];
2068
+ var dripText = {
2069
+ kind: "vector",
2070
+ id: "drip-text",
2071
+ name: "Drip text",
2072
+ description: "A chunky outlined title popping in, then oozing slime drips from its letters.",
2073
+ category: "icon",
2074
+ tags: ["title", "text", "drip", "slime", "y2k", "headline"],
2075
+ hold: 1200,
2076
+ presets: [
2077
+ { name: "Slime", values: { fill: [0.55, 0.93, 0.25], outline: [0.06, 0.2, 0.04], shine: [0.85, 1, 0.7] } },
2078
+ { name: "Molten gold", values: { fill: [1, 0.78, 0.2], outline: [0.35, 0.16, 0], shine: [1, 0.96, 0.75], drip: 1.5 } }
2079
+ ],
2080
+ // The word is stretched to a width set by its length (textLength), so the drips land under its letters in any font.
2081
+ derived: (v) => {
2082
+ const letters = Math.max(1, Array.from(String(v.word).trim()).length);
2083
+ const width = Math.min(380, letters * 75);
2084
+ const left = 200 - width / 2;
2085
+ return {
2086
+ width,
2087
+ ...Object.fromEntries(
2088
+ DRIPS.flatMap((d, i) => {
2089
+ const x = Math.round((left + d.at * width) * 10) / 10;
2090
+ const length = Math.round(d.length * Number(v.drip) * 10) / 10;
2091
+ return [
2092
+ [`x${i}`, x],
2093
+ [`left${i}`, x - d.width / 2],
2094
+ [`shine${i}`, x - d.width * 0.25],
2095
+ [`len${i}`, length],
2096
+ [`tip${i}`, DRIP_TOP + length]
2097
+ ];
2098
+ })
2099
+ )
2100
+ };
2101
+ },
2102
+ motion: (() => {
2103
+ const word = `<text x="200" y="120" text-anchor="middle" textLength="{{width}}" lengthAdjust="spacingAndGlyphs" font-family="'Arial Black', Impact, system-ui, sans-serif" font-size="104" font-weight="900" stroke-linejoin="round"`;
2104
+ const drips = (layer, paint) => DRIPS.map(
2105
+ (d, i) => `<rect id="${layer}r${i}" x="{{left${i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${paint}/><circle id="${layer}c${i}" cx="{{x${i}}}" cy="${DRIP_TOP}" r="${d.width * 0.72}" ${paint}/>`
2106
+ ).join("");
2107
+ const outline = `fill="{{outline}}" stroke="{{outline}}" stroke-width="{{stroke}}"`;
2108
+ const pop = (from, to) => [`translate(200 100) scale(${from}) translate(-200 -100)`, `translate(200 100) scale(${to}) translate(-200 -100)`];
2109
+ return {
2110
+ svg: `<svg viewBox="0 0 400 200"><!-- {{word: text default=SLOP label='Word'}} {{fill: color default=#FF7EB6 label='Fill'}} {{outline: color default=#3A0A2C label='Outline'}} {{shine: color default=#FFD1E6 label='Shine'}} {{drip: float min=0.2 max=2 default=1 label='Drip length'}} {{stroke: float min=0 max=14 default=7 step=0.5 label='Outline width'}} --><g id="word" transform="${pop(0.6, 1)[0]}" opacity="0">${drips("o", outline)}${word} fill="none" stroke="{{outline}}" stroke-width="{{stroke}}">{{word}}</text>${drips("f", `fill="{{fill}}"`)}${word} fill="{{fill}}">{{word}}</text>${DRIPS.map(
2111
+ (d, i) => `<circle id="s${i}" cx="{{shine${i}}}" cy="${DRIP_TOP}" r="${d.width * 0.2}" fill="{{shine}}" opacity="0"/>`
2112
+ ).join("")}</g></svg>`,
2113
+ animate: [
2114
+ { id: "word", attrs: { transform: pop(0.6, 1.08), opacity: [0, 1] }, ms: 260, ease: "out" },
2115
+ { id: "word", attrs: { transform: pop(1.08, 1) }, delay: 260, ms: 200, ease: "in-out" },
2116
+ ...DRIPS.flatMap((d, i) => {
2117
+ const at = { delay: 400 + d.delay, ms: d.ms, ease: "in-out" };
2118
+ const tip = `{{tip${i}}}`;
2119
+ return [
2120
+ ...["o", "f"].flatMap((layer) => [
2121
+ { id: `${layer}r${i}`, attrs: { height: [0, `{{len${i}}}`] }, ...at },
2122
+ { id: `${layer}c${i}`, attrs: { cy: [DRIP_TOP, tip] }, ...at }
2123
+ ]),
2124
+ { id: `s${i}`, attrs: { cy: [DRIP_TOP, tip], opacity: [0, 0.9] }, ...at }
2125
+ ];
2126
+ })
2127
+ ]
2128
+ };
2129
+ })()
2130
+ };
1788
2131
  var vectorEffects = [
1789
2132
  spinner,
1790
2133
  bouncingDots,
@@ -1800,7 +2143,9 @@ var vectorEffects = [
1800
2143
  heartbeat,
1801
2144
  signature,
1802
2145
  confetti,
1803
- blobMorph
2146
+ blobMorph,
2147
+ dripText,
2148
+ flowNetwork
1804
2149
  ];
1805
2150
 
1806
2151
  // src/catalog/index.ts
@@ -1823,10 +2168,12 @@ var SHADER_EFFECTS = [
1823
2168
  moire,
1824
2169
  kaleidoscope,
1825
2170
  barberStripes,
2171
+ neonCircuit,
1826
2172
  bokeh,
1827
2173
  godRays,
1828
2174
  sparkles,
1829
2175
  neonRipple,
2176
+ lightRibbons,
1830
2177
  starfield,
1831
2178
  nebula,
1832
2179
  blackHole,
@@ -1861,7 +2208,7 @@ function findEffects(query = {}) {
1861
2208
  });
1862
2209
  }
1863
2210
  function effectControls(effect) {
1864
- return effect.kind === "shader" ? parseControls(effect.source) : effect.controls.map((c, i) => vectorControlDef(c, i + 1));
2211
+ return effect.kind === "shader" ? parseControls(effect.source) : vectorEffectControls(effect);
1865
2212
  }
1866
2213
  function effectDefaults(effect, options = {}) {
1867
2214
  const preset = options.preset ? effect.presets?.find((p) => p.name === options.preset)?.values : void 0;
@@ -1880,6 +2227,10 @@ export {
1880
2227
  svgDataUrl,
1881
2228
  SAMPLE_IMAGES,
1882
2229
  EFFECT_CATEGORIES,
2230
+ VECTOR_CONTROL_TYPES,
2231
+ vectorControls,
2232
+ vectorEffectControls,
2233
+ vectorControlIssues,
1883
2234
  vectorControlDef,
1884
2235
  renderVectorEffect,
1885
2236
  colorControl,
@@ -1891,4 +2242,4 @@ export {
1891
2242
  effectControls,
1892
2243
  effectDefaults
1893
2244
  };
1894
- //# sourceMappingURL=chunk-MWVTQFV5.js.map
2245
+ //# sourceMappingURL=chunk-IWKM5FJK.js.map