@pixerate/flair 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,7 @@
1
+ import {
2
+ morphPaths,
3
+ morphSequence
4
+ } from "./chunk-OHW7UG6D.js";
1
5
  import {
2
6
  getTransitionGLSL
3
7
  } from "./chunk-EHDJ3CPJ.js";
@@ -295,10 +299,33 @@ var PORTRAIT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
295
299
  <circle cx="295" cy="150" r="6" fill="#2b1d16"/><circle cx="345" cy="150" r="6" fill="#2b1d16"/>
296
300
  <path d="M300 185 Q320 198 340 185" stroke="#7a3e2c" stroke-width="5" fill="none"/>
297
301
  </svg>`;
302
+ var PORTRAIT_2 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360" width="640" height="360">
303
+ <rect width="640" height="360" fill="#d3e4e6"/>
304
+ <path d="M255 360 Q268 300 430 297 Q592 300 605 360Z" fill="#a4372f"/>
305
+ <rect x="408" y="250" width="44" height="52" fill="#d9a07c"/>
306
+ <circle cx="430" cy="175" r="95" fill="#e8b48f"/>
307
+ <path d="M332 172 Q330 66 430 64 Q530 66 528 172 Q520 112 470 100 Q420 128 352 116 Q336 140 332 172Z" fill="#8a3b1e"/>
308
+ <circle cx="396" cy="175" r="8" fill="#3a2418"/><circle cx="464" cy="175" r="8" fill="#3a2418"/>
309
+ <path d="M424 186 Q430 206 436 186" stroke="#c48262" stroke-width="5" fill="none"/>
310
+ <path d="M403 225 Q430 244 457 225" stroke="#9c4a35" stroke-width="7" fill="none" stroke-linecap="round"/>
311
+ </svg>`;
312
+ var LOGO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
313
+ <path fill="#6366f1" fill-rule="evenodd" d="M256 56a200 200 0 1 1 0 400a200 200 0 1 1 0-400zm0 92a108 108 0 1 0 0 216a108 108 0 1 0 0-216z"/>
314
+ <circle cx="256" cy="256" r="52" fill="#6366f1"/>
315
+ </svg>`;
316
+ var HEART = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
317
+ <path fill="#f43f5e" d="M256 452C120 352 40 272 40 172C40 102 95 58 160 58C205 58 238 84 256 118C274 84 307 58 352 58C417 58 472 102 472 172C472 272 392 352 256 452Z"/>
318
+ </svg>`;
298
319
  var SAMPLE_IMAGES = {
299
320
  landscape: svgDataUrl(LANDSCAPE),
300
321
  city: svgDataUrl(CITY),
301
- portrait: svgDataUrl(PORTRAIT)
322
+ portrait: svgDataUrl(PORTRAIT),
323
+ /** Another face, larger and to the right: morph it with `portrait`. */
324
+ portrait2: svgDataUrl(PORTRAIT_2),
325
+ /** A ring-and-dot mark, indigo on transparent. */
326
+ logo: svgDataUrl(LOGO),
327
+ /** A heart, rose on transparent. */
328
+ heart: svgDataUrl(HEART)
302
329
  };
303
330
  function charWidth(c) {
304
331
  if (/[iIjl.,:;'!|`]/.test(c)) return 0.3;
@@ -1438,6 +1465,870 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1438
1465
  ]
1439
1466
  };
1440
1467
 
1468
+ // src/catalog/morph.ts
1469
+ var NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
1470
+ var DEG = Math.PI / 180;
1471
+ var r2 = (v) => {
1472
+ const r = Math.round(v * 100) / 100;
1473
+ return Object.is(r, -0) ? 0 : r;
1474
+ };
1475
+ var n = (name) => `{{${name}}}`;
1476
+ var pointsOf = (d) => {
1477
+ const nums = (d.match(NUMBER) ?? []).map(Number);
1478
+ return Array.from({ length: nums.length >> 1 }, (_, i) => [nums[2 * i], nums[2 * i + 1]]);
1479
+ };
1480
+ function mapPath(d, f) {
1481
+ const moved = pointsOf(d).map(f);
1482
+ let i = 0;
1483
+ return d.replace(NUMBER, () => {
1484
+ const k = i++;
1485
+ return String(r2(moved[k >> 1][k % 2]));
1486
+ });
1487
+ }
1488
+ var polyline = (pts, close = false) => `M${pts.map((p) => `${r2(p[0])} ${r2(p[1])}`).join(" L")}${close ? " Z" : ""}`;
1489
+ function arc(cx, cy, rx, ry, from, to, every = 8) {
1490
+ const steps = Math.max(1, Math.ceil(Math.abs(to - from) / every));
1491
+ return Array.from({ length: steps + 1 }, (_, k) => {
1492
+ const a = (from + (to - from) * k / steps) * DEG;
1493
+ return [cx + rx * Math.cos(a), cy + ry * Math.sin(a)];
1494
+ });
1495
+ }
1496
+ var circle = (cx, cy, r) => polyline(arc(cx, cy, r, r, 0, 360).slice(0, -1), true);
1497
+ var star = (cx, cy, tips, outer, inner) => polyline(
1498
+ Array.from({ length: tips * 2 }, (_, k) => {
1499
+ const r = k % 2 ? inner : outer;
1500
+ const a = (-90 + k * 180 / tips) * DEG;
1501
+ return [cx + r * Math.cos(a), cy + r * Math.sin(a)];
1502
+ }),
1503
+ true
1504
+ );
1505
+ function stroke(pts, width, flat = {}) {
1506
+ const h = width / 2;
1507
+ const normals = pts.slice(1).map((p, i) => {
1508
+ const q = pts[i];
1509
+ const l = Math.hypot(p[0] - q[0], p[1] - q[1]);
1510
+ return [-(p[1] - q[1]) / l, (p[0] - q[0]) / l];
1511
+ });
1512
+ const offsets = pts.map((_, i) => {
1513
+ const a = normals[Math.max(0, i - 1)];
1514
+ const b = normals[Math.min(normals.length - 1, i)];
1515
+ const l = Math.hypot(a[0] + b[0], a[1] + b[1]);
1516
+ const m = [(a[0] + b[0]) / l, (a[1] + b[1]) / l];
1517
+ const k = h / (m[0] * b[0] + m[1] * b[1]);
1518
+ return [m[0] * k, m[1] * k];
1519
+ });
1520
+ const left = pts.map((p, i) => [p[0] + offsets[i][0], p[1] + offsets[i][1]]);
1521
+ const right = pts.map((p, i) => [p[0] - offsets[i][0], p[1] - offsets[i][1]]).reverse();
1522
+ const angle = (v) => Math.atan2(v[1], v[0]) / DEG;
1523
+ const [first, last] = [pts[0], pts[pts.length - 1]];
1524
+ const endAt = angle(normals[normals.length - 1]);
1525
+ const startAt = angle(normals[0]) + 180;
1526
+ const endCap = flat.end ? [] : arc(last[0], last[1], h, h, endAt, endAt - 180, 15).slice(1, -1);
1527
+ const startCap = flat.start ? [] : arc(first[0], first[1], h, h, startAt, startAt - 180, 15).slice(1, -1);
1528
+ return polyline([...left, ...endCap, ...right, ...startCap], true);
1529
+ }
1530
+ function around(c, r, from, to, avoid) {
1531
+ let delta = ((to - from) % 360 + 360) % 360;
1532
+ if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
1533
+ return arc(c[0], c[1], r, r, from, from + delta);
1534
+ }
1535
+ function crescent(c1, r12, c2, r2_) {
1536
+ const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
1537
+ const d = Math.hypot(dx, dy);
1538
+ const a = (r12 * r12 - r2_ * r2_ + d * d) / (2 * d);
1539
+ const h = Math.sqrt(Math.max(0, r12 * r12 - a * a));
1540
+ const [px, py] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
1541
+ const i1 = [px - h * dy / d, py + h * dx / d];
1542
+ const i2 = [px + h * dy / d, py - h * dx / d];
1543
+ const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG;
1544
+ const outer = around(c1, r12, angle(c1, i1), angle(c1, i2), angle(c1, c2));
1545
+ const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
1546
+ return polyline([...outer, ...inner.slice(1, -1)], true);
1547
+ }
1548
+ var HEART2 = "M12 20.5 C12 20.5 3 14.8 3 8.8 C3 6 5.2 3.8 8 3.8 C9.7 3.8 11.2 4.7 12 6.1 C12.8 4.7 14.3 3.8 16 3.8 C18.8 3.8 21 6 21 8.8 C21 14.8 12 20.5 12 20.5 Z";
1549
+ var placed = (d, cx, cy, s) => mapPath(d, (p) => [cx + (p[0] - 12) * s, cy + (p[1] - 12) * s]);
1550
+ function fitted(d, box, points) {
1551
+ const outline = morphSequence([d], { points })[0];
1552
+ const pts = pointsOf(outline);
1553
+ const xs = pts.map((p) => p[0]);
1554
+ const ys = pts.map((p) => p[1]);
1555
+ const [x0, y0] = [Math.min(...xs), Math.min(...ys)];
1556
+ const [w, h] = [Math.max(...xs) - x0, Math.max(...ys) - y0];
1557
+ const s = Math.min(box[2] / Math.max(w, 1e-6), box[3] / Math.max(h, 1e-6));
1558
+ const [ox, oy] = [box[0] + (box[2] - w * s) / 2, box[1] + (box[3] - h * s) / 2];
1559
+ return mapPath(outline, (p) => [ox + (p[0] - x0) * s, oy + (p[1] - y0) * s]);
1560
+ }
1561
+ function fittedOr(d, fallback, box, points) {
1562
+ try {
1563
+ return fitted(String(d), box, points);
1564
+ } catch {
1565
+ return fitted(fallback, box, points);
1566
+ }
1567
+ }
1568
+ var SHAPE_FROM = "M30 12 H70 A18 18 0 0 1 88 30 V70 A18 18 0 0 1 70 88 H30 A18 18 0 0 1 12 70 V30 A18 18 0 0 1 30 12 Z";
1569
+ var SHAPE_TO = "M50 6 L61 38 L95 38 L67 58 L78 92 L50 72 L22 92 L33 58 L5 38 L39 38 Z";
1570
+ var shapeMorph = {
1571
+ kind: "vector",
1572
+ id: "shape-morph",
1573
+ name: "Shape morph",
1574
+ description: "Any two SVG paths morphing into each other and back, their fill and outline colours blending along.",
1575
+ category: "morph",
1576
+ tags: ["morph", "shape", "path", "icon", "logo"],
1577
+ hold: 0,
1578
+ presets: [
1579
+ { name: "Ink", values: { background: [0.98, 0.97, 0.94], fillFrom: [0.1, 0.1, 0.12], fillTo: [0.1, 0.1, 0.12], strokeFrom: [0.1, 0.1, 0.12], strokeTo: [0.1, 0.1, 0.12], width: 0 } },
1580
+ {
1581
+ name: "Heart to drop",
1582
+ values: {
1583
+ from: "M12 20.5 C12 20.5 3 14.8 3 8.8 C3 6 5.2 3.8 8 3.8 C9.7 3.8 11.2 4.7 12 6.1 C12.8 4.7 14.3 3.8 16 3.8 C18.8 3.8 21 6 21 8.8 C21 14.8 12 20.5 12 20.5 Z",
1584
+ to: "M12 2 C12 2 5 10 5 15 A7 7 0 0 0 19 15 C19 10 12 2 12 2 Z",
1585
+ fillFrom: [0.96, 0.25, 0.37],
1586
+ fillTo: [0.22, 0.6, 0.97]
1587
+ }
1588
+ },
1589
+ { name: "Outline", values: { fillFrom: [0.06, 0.09, 0.16], fillTo: [0.06, 0.09, 0.16], strokeFrom: [0.2, 0.83, 0.6], strokeTo: [0.98, 0.8, 0.08], width: 2.5 } }
1590
+ ],
1591
+ // Each path is fitted to the picture on its own, so paths in any coordinates fill it.
1592
+ derived: (v) => {
1593
+ const [a, b] = morphPaths(fittedOr(v.from, SHAPE_FROM, [12, 12, 76, 76], 96), fittedOr(v.to, SHAPE_TO, [12, 12, 76, 76], 96), { points: 80 });
1594
+ return { a, b, back: Number(v.pause) * 2 + Number(v.morph) };
1595
+ },
1596
+ motion: {
1597
+ svg: `<svg viewBox="0 0 100 100"><!-- {{from: text default='${SHAPE_FROM}' label='From path'}} {{to: text default='${SHAPE_TO}' label='To path'}} {{fillFrom: color default=#6366F1 label='Fill (from)'}} {{fillTo: color default=#F59E0B label='Fill (to)'}} {{strokeFrom: color default=#C7D2FE label='Outline (from)'}} {{strokeTo: color default=#FEF3C7 label='Outline (to)'}} {{background: color default=#0F172A label='Background'}} {{width: float min=0 max=6 default=1.5 step=0.25 label='Outline width'}} {{morph: float min=200 max=3000 default=1000 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=700 step=50 label='Pause ms'}} --><rect width="100" height="100" fill="{{background}}"/><path id="shape" d="{{a}}" fill="{{fillFrom}}" fill-rule="evenodd" stroke="{{strokeFrom}}" stroke-width="{{width}}" stroke-linejoin="round"/></svg>`,
1598
+ animate: [
1599
+ { id: "shape", attrs: { d: ["{{a}}", "{{b}}"], fill: ["{{fillFrom}}", "{{fillTo}}"], stroke: ["{{strokeFrom}}", "{{strokeTo}}"] }, delay: n("pause"), ms: n("morph"), ease: "in-out" },
1600
+ { id: "shape", attrs: { d: ["{{b}}", "{{a}}"], fill: ["{{fillTo}}", "{{fillFrom}}"], stroke: ["{{strokeTo}}", "{{strokeFrom}}"] }, delay: n("back"), ms: n("morph"), ease: "in-out" }
1601
+ ]
1602
+ }
1603
+ };
1604
+ var LINE = 2.4;
1605
+ var ICON_PAIRS = (() => {
1606
+ const c = 12;
1607
+ const meet = LINE / 2 / Math.SQRT2;
1608
+ const sun = [circle(c, c, 4.6), ...Array.from({ length: 8 }, (_, k) => {
1609
+ const a = k * 45 * DEG;
1610
+ return stroke([[c + 7.6 * Math.cos(a), c + 7.6 * Math.sin(a)], [c + 10 * Math.cos(a), c + 10 * Math.sin(a)]], 2);
1611
+ })];
1612
+ return [
1613
+ [
1614
+ "M7.5 4.5 L13.5 8.32 L13.5 15.68 L7.5 19.5 Z M13.5 8.32 L19.3 12 L13.5 15.68 Z",
1615
+ [stroke([[8.5, 6.5], [8.5, 17.5]], 3.6), stroke([[15.5, 6.5], [15.5, 17.5]], 3.6)].join(" ")
1616
+ ],
1617
+ [
1618
+ [7, 12, 17].map((y) => stroke([[5, y], [19, y]], LINE)).join(" "),
1619
+ [
1620
+ stroke([[6.5, 6.5], [17.5, 17.5]], LINE),
1621
+ stroke([[17.5, 6.5], [c + meet, c - meet]], LINE, { end: true }),
1622
+ stroke([[6.5, 17.5], [c - meet, c + meet]], LINE, { end: true })
1623
+ ].join(" ")
1624
+ ],
1625
+ ["M10.7 5 H13.3 V10.7 H19 V13.3 H13.3 V19 H10.7 V13.3 H5 V10.7 H10.7 Z", stroke([[5, 12.5], [10, 17.5], [19, 7]], 2.6)],
1626
+ [sun.join(" "), crescent([11.5, 12.5], 8.5, [16.5, 7.5], 7)],
1627
+ [`${HEART2} ${placed(HEART2, 12, 12, 0.7)}`, HEART2]
1628
+ ];
1629
+ })();
1630
+ var iconMorphs = /* @__PURE__ */ new Map();
1631
+ function iconMorph(pair2) {
1632
+ if (!iconMorphs.has(pair2)) iconMorphs.set(pair2, morphPaths(...ICON_PAIRS[pair2], { points: 64 }));
1633
+ return iconMorphs.get(pair2);
1634
+ }
1635
+ var pose = (turn, scale) => `translate(60 60) rotate(${turn}) scale(${scale}) translate(-12 -12)`;
1636
+ var toggle = (at, d, fill, turn) => [
1637
+ { id: "glyph", attrs: { d }, delay: at, ms: 420, ease: "in-out" },
1638
+ { id: "icon", attrs: { transform: [pose(0, 2.6), pose(turn, 2.15)] }, delay: at, ms: 170, ease: "out" },
1639
+ { id: "icon", attrs: { transform: [pose(turn, 2.15), pose(-turn * 0.3, 2.85)] }, delay: at + 170, ms: 190, ease: "in-out" },
1640
+ { id: "icon", attrs: { transform: [pose(-turn * 0.3, 2.85), pose(0, 2.6)] }, delay: at + 360, ms: 260, ease: "in-out" },
1641
+ { id: "button", attrs: { fill }, delay: at, ms: 320, ease: "in-out" }
1642
+ ];
1643
+ var iconToggle = {
1644
+ kind: "vector",
1645
+ id: "icon-toggle",
1646
+ name: "Icon toggle",
1647
+ description: "A round button whose icon morphs between two states (play and pause, menu and close, sun and moon) with a little pop.",
1648
+ category: "morph",
1649
+ tags: ["morph", "icon", "ui", "button", "toggle", "micro-interaction"],
1650
+ hold: 700,
1651
+ presets: [
1652
+ { name: "Play / pause", values: { pair: 0 } },
1653
+ { name: "Menu / close", values: { pair: 1, off: [0.98, 0.98, 0.98], on: [0.07, 0.07, 0.09], ink: [0.4, 0.4, 0.45], page: [0.9, 0.91, 0.93] } },
1654
+ { name: "Plus / check", values: { pair: 2, on: [0.13, 0.77, 0.37] } },
1655
+ { name: "Day / night", values: { pair: 3, off: [0.98, 0.75, 0.14], on: [0.19, 0.18, 0.5], page: [0.06, 0.07, 0.15] } },
1656
+ { name: "Like", values: { pair: 4, off: [0.12, 0.12, 0.14], on: [0.94, 0.27, 0.4] } }
1657
+ ],
1658
+ derived: (v) => {
1659
+ const [a, b] = iconMorph(Math.min(ICON_PAIRS.length - 1, Math.max(0, Math.round(Number(v.pair)) || 0)));
1660
+ return { a, b };
1661
+ },
1662
+ motion: {
1663
+ svg: `<svg viewBox="0 0 120 120"><!-- {{pair: int min=0 max=4 default=0 label='Icons (play, menu, plus, sun, heart)'}} {{ink: color default=#FFFFFF label='Icon'}} {{off: color default=#334155 label='Button'}} {{on: color default=#6366F1 label='Button (on)'}} {{page: color default=#0B1020 label='Background'}} --><rect width="120" height="120" fill="{{page}}"/><circle id="button" cx="60" cy="60" r="44" fill="{{off}}"/><g id="icon" transform="${pose(0, 2.6)}"><path id="glyph" d="{{a}}" fill="{{ink}}" fill-rule="evenodd" stroke="{{ink}}" stroke-width="0.3" stroke-linejoin="round"/></g></svg>`,
1664
+ animate: [...toggle(500, ["{{a}}", "{{b}}"], ["{{off}}", "{{on}}"], -16), ...toggle(2100, ["{{b}}", "{{a}}"], ["{{on}}", "{{off}}"], 16)]
1665
+ }
1666
+ };
1667
+ var SEQUENCE = morphSequence(
1668
+ [circle(100, 100, 72), star(100, 104, 5, 82, 36), placed(HEART2, 100, 98, 8.4), star(100, 100, 3, 78, 78)],
1669
+ { points: 96 }
1670
+ );
1671
+ var morphSequenceEffect = {
1672
+ kind: "vector",
1673
+ id: "morph-sequence",
1674
+ name: "Morph sequence",
1675
+ description: "One shape flowing from circle to star to heart to hexagon and round again, its colour changing with each.",
1676
+ category: "morph",
1677
+ tags: ["morph", "shape", "loop", "background", "playful"],
1678
+ hold: 0,
1679
+ presets: [
1680
+ { name: "Pastel", values: { background: [0.99, 0.97, 0.94], c0: [0.65, 0.84, 0.98], c1: [0.99, 0.85, 0.55], c2: [0.98, 0.65, 0.7], c3: [0.75, 0.7, 0.98] } },
1681
+ { name: "Mono", values: { background: [0.04, 0.04, 0.05], c0: [0.95, 0.95, 0.95], c1: [0.95, 0.95, 0.95], c2: [0.95, 0.95, 0.95], c3: [0.95, 0.95, 0.95], morph: 600, pause: 300 } }
1682
+ ],
1683
+ derived: (v) => {
1684
+ const each = Number(v.morph) + Number(v.pause);
1685
+ return Object.fromEntries(SEQUENCE.map((_, i) => [`at${i}`, Number(v.pause) + i * each]));
1686
+ },
1687
+ motion: {
1688
+ svg: `<svg viewBox="0 0 200 200"><!-- {{c0: color default=#38BDF8 label='Circle'}} {{c1: color default=#FACC15 label='Star'}} {{c2: color default=#F43F5E label='Heart'}} {{c3: color default=#A78BFA label='Hexagon'}} {{background: color default=#0F172A label='Background'}} {{morph: float min=200 max=3000 default=900 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=500 step=50 label='Pause ms'}} --><rect width="200" height="200" fill="{{background}}"/><path id="shape" d="${SEQUENCE[0]}" fill="{{c0}}" fill-rule="evenodd"/></svg>`,
1689
+ animate: SEQUENCE.map((d, i) => {
1690
+ const next = (i + 1) % SEQUENCE.length;
1691
+ return { id: "shape", attrs: { d: [d, SEQUENCE[next]], fill: [`{{c${i}}}`, `{{c${next}}}`] }, delay: n(`at${i}`), ms: n("morph"), ease: "in-out" };
1692
+ })
1693
+ }
1694
+ };
1695
+ var GOO = (() => {
1696
+ const cluster = [
1697
+ circle(100, 100, 25),
1698
+ ...Array.from({ length: 5 }, (_, k) => circle(100 + 56 * Math.cos((-90 + 72 * k) * DEG), 100 + 56 * Math.sin((-90 + 72 * k) * DEG), 15))
1699
+ ].join(" ");
1700
+ const blobStar = polyline(
1701
+ arc(100, 100, 1, 1, -90, 270, 4).slice(0, -1).map((p) => {
1702
+ const a = Math.atan2(p[1] - 100, p[0] - 100);
1703
+ const r = 50 + 20 * Math.cos(5 * (a + Math.PI / 2));
1704
+ return [100 + r * Math.cos(a), 100 + r * Math.sin(a)];
1705
+ }),
1706
+ true
1707
+ );
1708
+ const pair2 = `${circle(63, 100, 35)} ${circle(137, 100, 35)}`;
1709
+ const stages = morphSequence([cluster, blobStar, pair2], { points: 64 }).map((d) => d.split(/ (?=M)/));
1710
+ return stages[0].map((_, piece) => stages.map((stage) => stage[piece]));
1711
+ })();
1712
+ var gooeyMorph = {
1713
+ kind: "vector",
1714
+ id: "gooey-morph",
1715
+ name: "Gooey morph",
1716
+ description: "A cluster of liquid drops melting into a star, splitting into two blobs and beading back into drops.",
1717
+ category: "morph",
1718
+ tags: ["morph", "gooey", "liquid", "metaball", "blob", "loop"],
1719
+ hold: 0,
1720
+ presets: [
1721
+ { name: "Lava", values: { colorA: [1, 0.85, 0.2], colorB: [0.95, 0.2, 0.1], background: [0.1, 0.02, 0.02] } },
1722
+ { name: "Slime", values: { colorA: [0.75, 1, 0.3], colorB: [0.1, 0.7, 0.35], background: [0.03, 0.08, 0.05], goo: 9 } },
1723
+ { name: "Ink", values: { colorA: [0.1, 0.1, 0.12], colorB: [0.1, 0.1, 0.12], background: [0.97, 0.96, 0.93] } }
1724
+ ],
1725
+ derived: (v) => {
1726
+ const each = Number(v.morph) + Number(v.pause);
1727
+ return { ...Object.fromEntries(GOO[0].map((_, i) => [`at${i}`, Number(v.pause) + i * each])), threshold: -0.5 * Number(v.sharp) };
1728
+ },
1729
+ motion: {
1730
+ svg: `<svg viewBox="0 0 200 200"><!-- {{colorA: color default=#22D3EE label='Colour A'}} {{colorB: color default=#A855F7 label='Colour B'}} {{background: color default=#0B0B1A label='Background'}} {{goo: float min=1 max=14 default=7 step=0.5 label='Gooeyness'}} {{sharp: float min=8 max=40 default=20 step=1 label='Edge sharpness'}} {{morph: float min=300 max=3000 default=1200 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=400 step=50 label='Pause ms'}} --><defs><linearGradient id="gooFill" gradientUnits="userSpaceOnUse" x1="30" y1="30" x2="170" y2="170"><stop offset="0" stop-color="{{colorA}}"/><stop offset="1" stop-color="{{colorB}}"/></linearGradient><filter id="goo" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur in="SourceGraphic" stdDeviation="{{goo}}" result="soft"/><feColorMatrix in="soft" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 {{sharp}} {{threshold}}"/></filter></defs><rect width="200" height="200" fill="{{background}}"/><g filter="url(#goo)" fill="url(#gooFill)">${GOO.map((stages, i) => `<path id="goo${i}" d="${stages[0]}" fill-rule="evenodd"/>`).join("")}</g></svg>`,
1731
+ animate: GOO.flatMap(
1732
+ (stages, i) => stages.map((d, s) => ({ id: `goo${i}`, attrs: { d: [d, stages[(s + 1) % stages.length]] }, delay: n(`at${s}`), ms: n("morph"), ease: "in-out" }))
1733
+ )
1734
+ }
1735
+ };
1736
+ var DOTS = 48;
1737
+ var DOT_STAGGER = 14;
1738
+ var PARTICLE_FROM = HEART2;
1739
+ var PARTICLE_TO = star(12, 12, 5, 10, 4.2);
1740
+ var noise = (i) => {
1741
+ const x = Math.sin(i * 12.9898 + 4.1414) * 43758.5453;
1742
+ return x - Math.floor(x);
1743
+ };
1744
+ var particleMorph = {
1745
+ kind: "vector",
1746
+ id: "particle-morph",
1747
+ name: "Particle morph",
1748
+ description: "A shape bursting into dots along its outline that swirl across and settle into a second shape, then back.",
1749
+ category: "morph",
1750
+ tags: ["morph", "particles", "dots", "shape", "logo", "reveal"],
1751
+ hold: 600,
1752
+ presets: [
1753
+ { name: "Confetti", values: { background: [1, 0.98, 0.95], fromColor: [0.93, 0.27, 0.6], toColor: [0.15, 0.55, 0.95], ghost: 0.1, size: 3.2 } },
1754
+ { name: "Embers", values: { background: [0.05, 0.02, 0.02], fromColor: [1, 0.85, 0.3], toColor: [1, 0.3, 0.1], scatter: 50, size: 2 } },
1755
+ { name: "Circle to square", values: { from: "M12 2 A10 10 0 1 1 12 22 A10 10 0 1 1 12 2 Z", to: "M3 3 H21 V21 H3 Z" } }
1756
+ ],
1757
+ // The dots sit on the morphed outlines: point k of one shape flies to point k of the other.
1758
+ derived: (v) => {
1759
+ const box = [40, 40, 120, 120];
1760
+ const [a, b] = morphPaths(fittedOr(v.from, PARTICLE_FROM, box, 96), fittedOr(v.to, PARTICLE_TO, box, 96), { points: DOTS });
1761
+ const [pa, pb] = [pointsOf(a), pointsOf(b)];
1762
+ const out = { a, b };
1763
+ const scatter = Number(v.scatter);
1764
+ for (let i = 0; i < DOTS; i++) {
1765
+ const k = Math.floor(i * pa.length / DOTS);
1766
+ const [p, q] = [pa[k], pb[k]];
1767
+ out[`ax${i}`] = r2(p[0]);
1768
+ out[`ay${i}`] = r2(p[1]);
1769
+ out[`bx${i}`] = r2(q[0]);
1770
+ out[`by${i}`] = r2(q[1]);
1771
+ for (const [name, seed] of [["s", i], ["t", i + DOTS]]) {
1772
+ const angle = seed * 2.39996;
1773
+ const far = scatter * (0.45 + 0.55 * noise(seed));
1774
+ out[`${name}x${i}`] = r2((p[0] + q[0]) / 2 + far * Math.cos(angle));
1775
+ out[`${name}y${i}`] = r2((p[1] + q[1]) / 2 + far * Math.sin(angle));
1776
+ }
1777
+ }
1778
+ return out;
1779
+ },
1780
+ motion: (() => {
1781
+ const back = 3e3;
1782
+ const spread = DOTS * DOT_STAGGER;
1783
+ const leg = (i, at, from, mid, to, colours) => [
1784
+ { id: `p${i}`, attrs: { cx: [n(`${from}x${i}`), n(`${mid}x${i}`)], cy: [n(`${from}y${i}`), n(`${mid}y${i}`)] }, delay: at + i * DOT_STAGGER, ms: 450, ease: "out" },
1785
+ { id: `p${i}`, attrs: { cx: [n(`${mid}x${i}`), n(`${to}x${i}`)], cy: [n(`${mid}y${i}`), n(`${to}y${i}`)], fill: colours }, delay: at + 450 + i * DOT_STAGGER, ms: 650, ease: "in-out" }
1786
+ ];
1787
+ const fade = (id, at, show) => ({ id, attrs: { opacity: show ? [0, n("ghost")] : [n("ghost"), 0] }, delay: at, ms: 450, ease: "in-out" });
1788
+ return {
1789
+ svg: `<svg viewBox="0 0 200 200"><!-- {{from: text default='${PARTICLE_FROM}' label='From path'}} {{to: text default='${PARTICLE_TO}' label='To path'}} {{fromColor: color default=#F472B6 label='From colour'}} {{toColor: color default=#FBBF24 label='To colour'}} {{background: color default=#0C0A1D label='Background'}} {{size: float min=0.8 max=6 default=2.6 step=0.1 label='Dot size'}} {{scatter: float min=0 max=80 default=34 step=1 label='Scatter'}} {{ghost: float min=0 max=1 default=0.16 label='Shape fill'}} --><rect width="200" height="200" fill="{{background}}"/><path id="ghostA" d="{{a}}" fill="{{fromColor}}" fill-rule="evenodd" opacity="{{ghost}}"/><path id="ghostB" d="{{b}}" fill="{{toColor}}" fill-rule="evenodd" opacity="0"/>${Array.from(
1790
+ { length: DOTS },
1791
+ (_, i) => `<circle id="p${i}" cx="{{ax${i}}}" cy="{{ay${i}}}" r="{{size}}" fill="{{fromColor}}"/>`
1792
+ ).join("")}</svg>`,
1793
+ animate: [
1794
+ fade("ghostA", 400, false),
1795
+ fade("ghostB", 400 + spread + 900, true),
1796
+ fade("ghostB", back, false),
1797
+ fade("ghostA", back + spread + 900, true),
1798
+ ...Array.from({ length: DOTS }, (_, i) => [
1799
+ ...leg(i, 400, "a", "s", "b", ["{{fromColor}}", "{{toColor}}"]),
1800
+ ...leg(i, back, "b", "t", "a", ["{{toColor}}", "{{fromColor}}"])
1801
+ ]).flat()
1802
+ ]
1803
+ };
1804
+ })()
1805
+ };
1806
+ var DIGITS = (() => {
1807
+ const six = [...arc(17, 22, 14, 19, 245, 180), ...arc(10, 22, 7, 7, 180, 540).slice(1)];
1808
+ return [
1809
+ polyline(arc(10, 16, 7, 13, 0, 360).slice(0, -1), true),
1810
+ polyline([[5.5, 8], [11, 3], [11, 29]]),
1811
+ polyline([...arc(10, 9.5, 6.5, 6.5, 200, 380), [3.5, 29], [17, 29]]),
1812
+ polyline([...arc(10, 9, 6, 6, 215, 450), ...arc(10, 21.5, 7, 6.5, 270, 495).slice(1)]),
1813
+ polyline([[14, 29], [14, 3], [3, 20.5], [18, 20.5]]),
1814
+ polyline([[16.5, 3], [5.5, 3], [4.6, 14.6], ...arc(10.5, 21.2, 6.8, 7, 218, 500)]),
1815
+ polyline(six),
1816
+ polyline([[3, 3], [17, 3], [8, 29]]),
1817
+ polyline([...arc(10, 9.5, 5.5, 5.5, 90, 450), ...arc(10, 22, 7, 7, 270, 630).slice(1)]),
1818
+ polyline(six.map((p) => [20 - p[0], 32 - p[1]]))
1819
+ ];
1820
+ })();
1821
+ var BLANK = "M10 16 L10 16";
1822
+ var SLOTS = 4;
1823
+ var MAX_STEPS = 8;
1824
+ var DIGIT_REST = 260;
1825
+ var digitMorph = {
1826
+ kind: "vector",
1827
+ id: "digit-morph",
1828
+ name: "Digit morph",
1829
+ description: "A number counting from one value to another, each digit's stroke bending smoothly into the next.",
1830
+ category: "morph",
1831
+ tags: ["morph", "number", "counter", "countdown", "kpi", "digits"],
1832
+ hold: 900,
1833
+ presets: [
1834
+ { name: "Countdown", values: { from: 10, to: 0, ink: [1, 0.36, 0.3] } },
1835
+ { name: "New year", values: { from: 2025, to: 2026, morph: 700, background: [0.04, 0.03, 0.1], ink: [1, 0.84, 0.4] } },
1836
+ { name: "Thin", values: { weight: 1.4, background: [0.98, 0.98, 0.97], ink: [0.1, 0.1, 0.12] } }
1837
+ ],
1838
+ derived: (v) => {
1839
+ const from = Math.round(Number(v.from)) || 0;
1840
+ const to = Math.round(Number(v.to)) || 0;
1841
+ const steps = Math.min(MAX_STEPS, Math.abs(to - from));
1842
+ const values = Array.from({ length: steps + 1 }, (_, k) => Math.round(from + (to - from) * k / Math.max(1, steps)));
1843
+ const used = Math.max(1, ...values.map((x) => String(x).length));
1844
+ const scale = Math.min(4.2, 340 / (used * 22 - 2));
1845
+ const left = 200 - (used * 22 - 2) * scale / 2;
1846
+ const interval = Number(v.morph) + DIGIT_REST;
1847
+ const lastEnd = Math.max(0, steps - 1) * interval + Number(v.morph);
1848
+ const out = { scale: r2(scale), y: r2(100 - 16 * scale), back: lastEnd + DIGIT_REST * 3 };
1849
+ for (let k = 1; k <= MAX_STEPS; k++) out[`t${k}`] = k <= steps ? (k - 1) * interval : lastEnd;
1850
+ for (let j = 0; j < SLOTS; j++) {
1851
+ const place = j - (SLOTS - used);
1852
+ out[`x${j}`] = r2(left + Math.max(0, place) * 22 * scale);
1853
+ const glyphs = values.map((x) => {
1854
+ const text = String(x);
1855
+ const at = text.length - (used - place);
1856
+ return place >= 0 && at >= 0 ? Number(text[at]) : -1;
1857
+ });
1858
+ const paths = morphSequence(glyphs.map((g) => g < 0 ? BLANK : DIGITS[g]), { points: 64 });
1859
+ for (let k = 0; k <= MAX_STEPS; k++) {
1860
+ const s = Math.min(k, steps);
1861
+ out[`d${j}_${k}`] = paths[s];
1862
+ out[`o${j}_${k}`] = glyphs[s] < 0 ? 0 : 1;
1863
+ }
1864
+ }
1865
+ return out;
1866
+ },
1867
+ motion: {
1868
+ svg: `<svg viewBox="0 0 400 200"><!-- {{from: int min=0 max=9999 default=12 label='From'}} {{to: int min=0 max=9999 default=20 label='To'}} {{ink: color default=#F8FAFC label='Digits'}} {{background: color default=#111827 label='Background'}} {{weight: float min=0.8 max=5 default=2.8 step=0.1 label='Weight'}} {{morph: float min=150 max=1500 default=420 step=10 label='Morph ms'}} --><rect width="400" height="200" fill="{{background}}"/>${Array.from(
1869
+ { length: SLOTS },
1870
+ (_, j) => `<path id="s${j}" d="{{d${j}_0}}" opacity="{{o${j}_0}}" transform="translate({{x${j}}} {{y}}) scale({{scale}})" fill="none" fill-rule="evenodd" stroke="{{ink}}" stroke-width="{{weight}}" stroke-linecap="round" stroke-linejoin="round"/>`
1871
+ ).join("")}</svg>`,
1872
+ animate: Array.from({ length: SLOTS }, (_, j) => [
1873
+ ...Array.from({ length: MAX_STEPS }, (_2, i) => {
1874
+ const k = i + 1;
1875
+ return { id: `s${j}`, attrs: { d: [`{{d${j}_${k - 1}}}`, `{{d${j}_${k}}}`], opacity: [n(`o${j}_${k - 1}`), n(`o${j}_${k}`)] }, delay: n(`t${k}`), ms: n("morph"), ease: "in-out" };
1876
+ }),
1877
+ // Back to the first number in one morph, so it loops.
1878
+ { id: `s${j}`, attrs: { d: [`{{d${j}_${MAX_STEPS}}}`, `{{d${j}_0}}`], opacity: [n(`o${j}_${MAX_STEPS}`), n(`o${j}_0`)] }, delay: n("back"), ms: n("morph"), ease: "in-out" }
1879
+ ]).flat()
1880
+ }
1881
+ };
1882
+ var morphEffects = [shapeMorph, iconToggle, morphSequenceEffect, gooeyMorph, particleMorph, digitMorph];
1883
+
1884
+ // src/catalog/shaders/morph.ts
1885
+ var pair = (from, to) => `uniform sampler2D uFrom; // @control filter=linear wrap=clamp default=${from} label="From image"
1886
+ uniform sampler2D uTo; // @control filter=linear wrap=clamp default=${to} label="To image"
1887
+ uniform bool uAnimate; // @control default=true label="Animate"
1888
+ uniform float uProgress; // @control min=0 max=1 default=0.5 label="Progress (when still)"`;
1889
+ var COMMON = `
1890
+ float progress() {
1891
+ return uAnimate ? smoothstep(0.1, 0.9, 0.5 - 0.5 * cos(iTime * 6.28318 / 4.0)) : uProgress;
1892
+ }
1893
+
1894
+ // The share of an image visible across and up when it covers the frame (the test card fits it).
1895
+ vec2 coverScale(sampler2D tex) {
1896
+ vec2 size = vec2(textureSize(tex, 0));
1897
+ float frameAspect = iResolution.x / iResolution.y;
1898
+ float imageAspect = size.x > 1.0 ? size.x / size.y : frameAspect;
1899
+ return frameAspect > imageAspect ? vec2(1.0, imageAspect / frameAspect) : vec2(frameAspect / imageAspect, 1.0);
1900
+ }
1901
+
1902
+ vec3 cover(sampler2D tex, vec2 uv) {
1903
+ return picture(tex, 0.5 + (uv - 0.5) * coverScale(tex));
1904
+ }
1905
+
1906
+ float luma(vec3 c) {
1907
+ return dot(c, vec3(0.2126, 0.7152, 0.0722));
1908
+ }`.trim();
1909
+ var featureMorph = {
1910
+ kind: "shader",
1911
+ id: "feature-morph",
1912
+ name: "Feature morph",
1913
+ description: "The classic face morph: two pictures warp so five matching points (eyes, mouth, chin, hairline) meet while they cross-fade.",
1914
+ category: "morph",
1915
+ tags: ["morph", "face", "warp", "image", "transition"],
1916
+ loop: 4,
1917
+ source: glsl(
1918
+ `// Feature morph: both images warp towards in-between positions of five matching points while they cross-fade.
1919
+ ${pair(SAMPLE_IMAGES.portrait, SAMPLE_IMAGES.portrait2)}
1920
+ uniform vec2 uFromA; // @control min=0 max=1 default=0.461,0.583 label="From: left eye"
1921
+ uniform vec2 uFromB; // @control min=0 max=1 default=0.539,0.583 label="From: right eye"
1922
+ uniform vec2 uFromC; // @control min=0 max=1 default=0.5,0.472 label="From: mouth"
1923
+ uniform vec2 uFromD; // @control min=0 max=1 default=0.5,0.389 label="From: chin"
1924
+ uniform vec2 uFromE; // @control min=0 max=1 default=0.5,0.778 label="From: hairline"
1925
+ uniform vec2 uToA; // @control min=0 max=1 default=0.619,0.514 label="To: left eye"
1926
+ uniform vec2 uToB; // @control min=0 max=1 default=0.725,0.514 label="To: right eye"
1927
+ uniform vec2 uToC; // @control min=0 max=1 default=0.672,0.364 label="To: mouth"
1928
+ uniform vec2 uToD; // @control min=0 max=1 default=0.672,0.25 label="To: chin"
1929
+ uniform vec2 uToE; // @control min=0 max=1 default=0.672,0.778 label="To: hairline"
1930
+ uniform bool uShowPoints; // @control default=false label="Show points"`,
1931
+ PICTURE,
1932
+ COMMON,
1933
+ `// Points are given in image uv; the warp works in frame space (height 1, x scaled by the aspect).
1934
+ vec2 toFrame(vec2 imageUv, vec2 scale) {
1935
+ return (0.5 + (imageUv - 0.5) / scale) * vec2(iResolution.x / iResolution.y, 1.0);
1936
+ }
1937
+
1938
+ vec2 toImage(vec2 q, vec2 scale) {
1939
+ return 0.5 + (q / vec2(iResolution.x / iResolution.y, 1.0) - 0.5) * scale;
1940
+ }
1941
+
1942
+ vec2 cmul(vec2 a, vec2 b) {
1943
+ return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
1944
+ }
1945
+
1946
+ // Where frame point x, among the in-between points m, comes from in a picture whose points are s:
1947
+ // a best-fit similarity (move, scale, turn) carries m onto s, then inverse-distance weighting of
1948
+ // what is left over bends each point exactly onto its partner.
1949
+ vec2 warp(vec2 x, vec2 m[5], vec2 s[5]) {
1950
+ vec2 cm = vec2(0.0);
1951
+ vec2 cs = vec2(0.0);
1952
+ for (int i = 0; i < 5; i++) {
1953
+ cm += m[i] * 0.2;
1954
+ cs += s[i] * 0.2;
1955
+ }
1956
+ vec2 num = vec2(0.0);
1957
+ float den = 0.0;
1958
+ for (int i = 0; i < 5; i++) {
1959
+ vec2 a = m[i] - cm;
1960
+ num += cmul(vec2(a.x, -a.y), s[i] - cs);
1961
+ den += dot(a, a);
1962
+ }
1963
+ vec2 k = num / max(den, 1e-6);
1964
+ vec2 bend = vec2(0.0);
1965
+ float total = 0.0;
1966
+ for (int i = 0; i < 5; i++) {
1967
+ vec2 left = s[i] - (cs + cmul(k, m[i] - cm));
1968
+ float d2 = dot(x - m[i], x - m[i]);
1969
+ float w = 1.0 / (d2 * d2 + 1e-6);
1970
+ bend += w * left;
1971
+ total += w;
1972
+ }
1973
+ return cs + cmul(k, x - cm) + bend / total;
1974
+ }
1975
+
1976
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1977
+ float t = progress();
1978
+ vec2 scaleA = coverScale(uFrom);
1979
+ vec2 scaleB = coverScale(uTo);
1980
+ vec2 a[5] = vec2[5](toFrame(uFromA, scaleA), toFrame(uFromB, scaleA), toFrame(uFromC, scaleA), toFrame(uFromD, scaleA), toFrame(uFromE, scaleA));
1981
+ vec2 b[5] = vec2[5](toFrame(uToA, scaleB), toFrame(uToB, scaleB), toFrame(uToC, scaleB), toFrame(uToD, scaleB), toFrame(uToE, scaleB));
1982
+ vec2 m[5];
1983
+ for (int i = 0; i < 5; i++) m[i] = mix(a[i], b[i], t);
1984
+ vec2 x = fragCoord / iResolution.y;
1985
+ vec3 colA = picture(uFrom, toImage(warp(x, m, a), scaleA));
1986
+ vec3 colB = picture(uTo, toImage(warp(x, m, b), scaleB));
1987
+ vec3 col = mix(colA, colB, smoothstep(0.2, 0.8, t));
1988
+ if (uShowPoints) {
1989
+ float px = 1.0 / iResolution.y;
1990
+ for (int i = 0; i < 5; i++) {
1991
+ float d = length(x - m[i]);
1992
+ col = mix(col, vec3(0.0), smoothstep(px * 1.5, 0.0, abs(d - 0.012)) * 0.6);
1993
+ col = mix(col, vec3(1.0, 0.85, 0.2), smoothstep(px, 0.0, abs(d - 0.012)));
1994
+ }
1995
+ }
1996
+ fragColor = vec4(col, 1.0);
1997
+ }`
1998
+ ),
1999
+ presets: [{ name: "Show points", values: { uShowPoints: true, uAnimate: false, uProgress: 0.5 } }]
2000
+ };
2001
+ var silhouetteMorph = {
2002
+ kind: "shader",
2003
+ id: "silhouette-morph",
2004
+ name: "Silhouette morph",
2005
+ description: "One silhouette or logo flowing into another as a gooey blob, its colour blending from the first shape's to the second's.",
2006
+ category: "morph",
2007
+ tags: ["morph", "shape", "logo", "goo", "blob", "text"],
2008
+ loop: 4,
2009
+ source: glsl(
2010
+ `// Silhouette morph: blurred alpha masks of two shapes mixed by progress and thresholded, so one flows into the other.
2011
+ uniform sampler2D uFrom; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.logo} label="From shape"
2012
+ uniform sampler2D uTo; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.heart} label="To shape"
2013
+ uniform bool uAnimate; // @control default=true label="Animate"
2014
+ uniform float uProgress; // @control min=0 max=1 default=0.5 label="Progress (when still)"
2015
+ uniform float uSize; // @control min=0.2 max=1 default=0.75 label="Shape size"
2016
+ uniform float uGoo; // @control min=0 max=0.3 default=0.1 label="Gooeyness"
2017
+ uniform float uEdge; // @control min=0 max=0.05 default=0.004 label="Edge softness"
2018
+ uniform bool uImageColours; // @control default=true label="Use the shapes' own colours"
2019
+ uniform vec3 uColourA; // @control color default=#6366F1 label="From colour"
2020
+ uniform vec3 uColourB; // @control color default=#F43F5E label="To colour"
2021
+ uniform vec3 uBackground; // @control color default=#0F172A label="Background"`,
2022
+ `// A shape's colour (premultiplied) and alpha at frame point p (height 1, centred), the image contained in
2023
+ // uSize of the frame, read from a mip level about as coarse as the spacing (in frame units), so sparse blur
2024
+ // taps still sum smoothly. With no image, a disc (fallback 0) or a rounded square (1).
2025
+ vec4 shapeAt(sampler2D tex, vec2 p, float spacing, int fallback) {
2026
+ vec2 size = vec2(textureSize(tex, 0));
2027
+ if (size.x <= 1.0) {
2028
+ float d = fallback == 0 ? length(p) - 0.3 * uSize : length(max(abs(p) - 0.2 * uSize, 0.0)) - 0.08 * uSize;
2029
+ float a = step(d, 0.0);
2030
+ return vec4(vec3(a), a);
2031
+ }
2032
+ float aspect = size.x / size.y;
2033
+ float h = min(uSize, iResolution.x / iResolution.y * uSize / aspect);
2034
+ vec2 uv = p / vec2(h * aspect, h) + 0.5;
2035
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return vec4(0.0);
2036
+ vec4 c = textureLod(tex, uv, log2(max(spacing * size.y / h, 1.0)));
2037
+ // Clear texels upload black, so filtered colour is already about colour \xD7 alpha: divide it out, then weight by alpha.
2038
+ return vec4(clamp(c.rgb / max(c.a, 1e-3), 0.0, 1.0) * c.a, c.a);
2039
+ }
2040
+
2041
+ // A disc blur of the shape: 28 taps on a golden-angle spiral.
2042
+ vec4 blurred(sampler2D tex, vec2 p, float radius, int fallback) {
2043
+ vec4 sum = vec4(0.0);
2044
+ for (int i = 0; i < 28; i++) {
2045
+ float r = radius * sqrt((float(i) + 0.5) / 28.0);
2046
+ float a = float(i) * 2.39996;
2047
+ sum += shapeAt(tex, p + r * vec2(cos(a), sin(a)), radius * 0.35, fallback);
2048
+ }
2049
+ return sum / 28.0;
2050
+ }
2051
+
2052
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2053
+ float t = uAnimate ? smoothstep(0.1, 0.9, 0.5 - 0.5 * cos(iTime * 6.28318 / 4.0)) : uProgress;
2054
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
2055
+ // Shapes are sharp at either end and softest half way, where the blobs merge.
2056
+ float radius = uGoo * sin(3.14159 * t) + 1.0 / iResolution.y;
2057
+ vec4 a = blurred(uFrom, p, radius, 0);
2058
+ vec4 b = blurred(uTo, p, radius, 1);
2059
+ float field = mix(a.a, b.a, t);
2060
+ float w = uEdge + fwidth(field) * 0.75;
2061
+ float inside = smoothstep(0.5 - w, 0.5 + w, field);
2062
+ // Each shape's colour here (or the other's, where only one shape reaches), blended by progress.
2063
+ vec3 ca = uImageColours ? a.rgb / max(a.a, 1e-4) : uColourA;
2064
+ vec3 cb = uImageColours ? b.rgb / max(b.a, 1e-4) : uColourB;
2065
+ ca = mix(cb, ca, smoothstep(0.0, 0.05, a.a));
2066
+ cb = mix(ca, cb, smoothstep(0.0, 0.05, b.a));
2067
+ vec3 tint = mix(ca, cb, smoothstep(0.2, 0.8, t));
2068
+ // A little roundness: the thick middle of a blob is lighter than its rim.
2069
+ tint *= 0.82 + 0.25 * smoothstep(0.5, 1.0, field);
2070
+ fragColor = vec4(mix(uBackground, tint, inside), 1.0);
2071
+ }`
2072
+ ),
2073
+ presets: [
2074
+ {
2075
+ name: "Hello \u2192 World",
2076
+ values: { uFrom: textImage("HELLO"), uTo: textImage("WORLD"), uImageColours: false, uSize: 0.9, uGoo: 0.05, uColourA: [0.98, 0.75, 0.14], uColourB: [0.13, 0.83, 0.93] }
2077
+ },
2078
+ { name: "Blobby", values: { uGoo: 0.25, uBackground: [1, 0.97, 0.93] } }
2079
+ ]
2080
+ };
2081
+ var transitionLiquidMorph = {
2082
+ kind: "shader",
2083
+ id: "transition-liquid-morph",
2084
+ name: "Liquid morph",
2085
+ description: "Both images swept along a flowing, swirling current that peaks half way, dissolving one into the other like ink in water.",
2086
+ category: "transition",
2087
+ tags: ["transition", "morph", "liquid", "image", "flow"],
2088
+ loop: 4,
2089
+ source: glsl(
2090
+ `// Liquid morph: both images advected along a domain-warped fbm current, strongest at mid-progress, while cross-fading.
2091
+ ${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
2092
+ uniform float uStrength; // @control min=0 max=0.5 default=0.1 label="Strength"
2093
+ uniform float uScale; // @control min=0.5 max=8 default=2.5 label="Scale"
2094
+ uniform float uFlow; // @control min=0 max=2 default=0.4 label="Flow speed"`,
2095
+ HASH,
2096
+ NOISE,
2097
+ FBM,
2098
+ PICTURE,
2099
+ COMMON,
2100
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2101
+ vec2 uv = fragCoord / iResolution.xy;
2102
+ float t = progress();
2103
+ float s = sin(3.14159 * t);
2104
+ float time = iTime * uFlow;
2105
+ vec2 q = fragCoord / iResolution.y * uScale;
2106
+ vec2 first = vec2(fbm(q + vec2(0.0, time), 4), fbm(q + vec2(5.2, 1.3) - vec2(time, 0.0), 4)) - 0.5;
2107
+ vec2 flow = vec2(fbm(q + 4.0 * first + vec2(1.7, 9.2) + time * 0.5, 4), fbm(q + 4.0 * first + vec2(8.3, 2.8) - time * 0.5, 4)) - 0.5;
2108
+ vec2 d = flow * uStrength * s * 2.0;
2109
+ // From is carried downstream, To arrives from upstream; neither moves at the ends.
2110
+ vec3 a = cover(uFrom, uv + d);
2111
+ vec3 b = cover(uTo, uv - d);
2112
+ float k = smoothstep(0.25, 0.75, t + (flow.x + flow.y) * 0.6 * s);
2113
+ fragColor = vec4(mix(a, b, k), 1.0);
2114
+ }`
2115
+ ),
2116
+ presets: [
2117
+ { name: "Gentle", values: { uStrength: 0.08, uScale: 1.5, uFlow: 0.2 } },
2118
+ { name: "Turbulent", values: { uStrength: 0.35, uScale: 4, uFlow: 1.2 } }
2119
+ ]
2120
+ };
2121
+ var transitionDisplacement = {
2122
+ kind: "shader",
2123
+ id: "transition-displacement",
2124
+ name: "Displacement",
2125
+ description: "Each image's brightness pushes it out in one direction as the next slides in, warped by its own light and dark.",
2126
+ category: "transition",
2127
+ tags: ["transition", "morph", "displacement", "image", "warp"],
2128
+ loop: 4,
2129
+ source: glsl(
2130
+ `// Displacement: From's luminance drives a directional warp out, To's luminance the same warp in reverse.
2131
+ ${pair(SAMPLE_IMAGES.city, SAMPLE_IMAGES.landscape)}
2132
+ uniform float uStrength; // @control min=0 max=1 default=0.4 label="Strength"
2133
+ uniform float uAngle; // @control min=0 max=360 default=0 label="Direction (\xB0)"`,
2134
+ PICTURE,
2135
+ COMMON,
2136
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2137
+ vec2 uv = fragCoord / iResolution.xy;
2138
+ float t = progress();
2139
+ vec2 dir = vec2(cos(radians(uAngle)), sin(radians(uAngle))) * vec2(iResolution.y / iResolution.x, 1.0);
2140
+ float la = luma(cover(uFrom, uv));
2141
+ float lb = luma(cover(uTo, uv));
2142
+ vec3 a = cover(uFrom, uv + dir * t * uStrength * la);
2143
+ vec3 b = cover(uTo, uv - dir * (1.0 - t) * uStrength * lb);
2144
+ fragColor = vec4(mix(a, b, smoothstep(0.2, 0.8, t)), 1.0);
2145
+ }`
2146
+ ),
2147
+ presets: [
2148
+ { name: "Upward smear", values: { uStrength: 0.6, uAngle: 90 } },
2149
+ { name: "Subtle", values: { uStrength: 0.12, uAngle: 30 } }
2150
+ ]
2151
+ };
2152
+ var transitionLuma = {
2153
+ kind: "shader",
2154
+ id: "transition-luma",
2155
+ name: "Luma fade",
2156
+ description: "Pixels switch to the next image in order of brightness, shadows first or highlights first, along a soft edge.",
2157
+ category: "transition",
2158
+ tags: ["transition", "morph", "luma", "image", "dissolve"],
2159
+ loop: 4,
2160
+ source: glsl(
2161
+ `// Luma fade: each pixel switches when progress passes its brightness, with a soft edge.
2162
+ ${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
2163
+ uniform float uSoftness; // @control min=0.01 max=0.5 default=0.1 label="Softness"
2164
+ uniform bool uLightFirst; // @control default=false label="Light first"
2165
+ uniform int uSource; // @control min=0 max=1 default=0 label="Brightness of (0 From, 1 To)"`,
2166
+ PICTURE,
2167
+ COMMON,
2168
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2169
+ vec2 uv = fragCoord / iResolution.xy;
2170
+ float t = progress();
2171
+ vec3 a = cover(uFrom, uv);
2172
+ vec3 b = cover(uTo, uv);
2173
+ float l = clamp(luma(uSource == 0 ? a : b), 0.0, 1.0);
2174
+ if (uLightFirst) l = 1.0 - l;
2175
+ // The front sweeps from below the darkest pixel to above the lightest, so both ends are clean.
2176
+ float front = mix(-uSoftness, 1.0 + uSoftness, t);
2177
+ fragColor = vec4(mix(a, b, smoothstep(l - uSoftness, l + uSoftness, front)), 1.0);
2178
+ }`
2179
+ ),
2180
+ presets: [
2181
+ { name: "Highlights first", values: { uLightFirst: true, uSoftness: 0.06 } },
2182
+ { name: "Hard threshold", values: { uSoftness: 0.01, uSource: 1 } }
2183
+ ]
2184
+ };
2185
+ var transitionParticleSwap = {
2186
+ kind: "shader",
2187
+ id: "transition-particle-swap",
2188
+ name: "Particle swap",
2189
+ description: "The image breaks into square particles that tumble along curved, staggered paths and land as the next image.",
2190
+ category: "transition",
2191
+ tags: ["transition", "morph", "particles", "image", "mosaic"],
2192
+ loop: 4,
2193
+ source: glsl(
2194
+ `// Particle swap: each cell of From flies to a neighbouring cell's place, shrinking and spinning on the way, and lands as To.
2195
+ ${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
2196
+ uniform float uParticle; // @control min=6 max=80 default=20 label="Particle size (px)"
2197
+ uniform float uScatter; // @control min=0 max=0.8 default=0.45 label="Scatter"
2198
+ uniform float uSpin; // @control min=0 max=2 default=0.6 label="Spin & curl"
2199
+ uniform vec3 uBackground; // @control color default=#0B0B12 label="Background"`,
2200
+ HASH,
2201
+ ROTATE,
2202
+ PICTURE,
2203
+ COMMON,
2204
+ `const vec2 RING[4] = vec2[4](vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0));
2205
+
2206
+ // Where cell c's particle lands: cells trade places around each 2 \xD7 2 block, turned 1\u20133 steps.
2207
+ vec2 landing(vec2 c) {
2208
+ vec2 block = floor(c * 0.5);
2209
+ vec2 local = c - block * 2.0;
2210
+ int i = local.y < 0.5 ? int(local.x) : 3 - int(local.x);
2211
+ int turn = 1 + int(hash12(block + 7.3) * 2.999);
2212
+ return block * 2.0 + RING[(i + turn) % 4];
2213
+ }
2214
+
2215
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2216
+ float t = progress();
2217
+ vec2 p = fragCoord / uParticle;
2218
+ vec2 grid = iResolution.xy / uParticle;
2219
+ vec2 base = floor(p);
2220
+ vec3 col = uBackground;
2221
+ float top = -1.0;
2222
+ vec2 hitFrom = vec2(0.0);
2223
+ vec2 hitTo = vec2(0.0);
2224
+ float hitT = 0.0;
2225
+ float hitCover = 0.0;
2226
+ // Particles travel at most about 1.5 cells, so the 5 \xD7 5 cells around are every candidate.
2227
+ for (int j = -2; j <= 2; j++) {
2228
+ for (int i = -2; i <= 2; i++) {
2229
+ vec2 c = base + vec2(float(i), float(j));
2230
+ vec2 h = hash22(c);
2231
+ // Staggered: a sweep from the left plus some randomness.
2232
+ float delay = 0.5 * (0.55 * clamp(c.x / grid.x, 0.0, 1.0) + 0.45 * h.x);
2233
+ float k = smoothstep(0.0, 1.0, clamp((t - delay) / 0.5, 0.0, 1.0));
2234
+ float lift = sin(3.14159 * k);
2235
+ vec2 from = c + 0.5;
2236
+ vec2 to = landing(c) + 0.5;
2237
+ vec2 path = to - from;
2238
+ vec2 side = normalize(vec2(-path.y, path.x) + (h - 0.5) * 0.8);
2239
+ vec2 pos = mix(from, to, k) + side * lift * (uScatter + uSpin * 0.3) * (h.y > 0.5 ? 0.6 : -0.6);
2240
+ float size = 1.0 - 0.3 * lift;
2241
+ float angle = (h.y - 0.5) * 6.28318 * uSpin * lift;
2242
+ vec2 l = rot(-angle) * (p - pos) / size;
2243
+ float m = max(abs(l.x), abs(l.y));
2244
+ float z = h.y + lift;
2245
+ if (m <= 0.5 && z > top) {
2246
+ top = z;
2247
+ hitFrom = from + l;
2248
+ hitTo = to + l;
2249
+ hitT = k;
2250
+ hitCover = 1.0 - smoothstep(0.5 - 1.5 / uParticle, 0.5, m) * min(lift * 8.0, 1.0);
2251
+ }
2252
+ }
2253
+ }
2254
+ if (top > -1.0) {
2255
+ vec3 a = cover(uFrom, hitFrom / grid);
2256
+ vec3 b = cover(uTo, hitTo / grid);
2257
+ vec3 particle = mix(a, b, hitT) * (1.0 + 0.15 * sin(3.14159 * hitT));
2258
+ col = mix(col, particle, hitCover);
2259
+ }
2260
+ fragColor = vec4(col, 1.0);
2261
+ }`
2262
+ ),
2263
+ presets: [
2264
+ { name: "Confetti", values: { uParticle: 10, uScatter: 0.8, uSpin: 1.6 } },
2265
+ { name: "Tiles", values: { uParticle: 48, uScatter: 0.2, uSpin: 0.15 } }
2266
+ ]
2267
+ };
2268
+ var transitionInkBleed = {
2269
+ kind: "shader",
2270
+ id: "transition-ink-bleed",
2271
+ name: "Ink bleed",
2272
+ description: "The next image bleeds through the first like wet ink on paper, spreading from a few seeds with dark, pooled edges.",
2273
+ category: "transition",
2274
+ tags: ["transition", "morph", "ink", "watercolour", "image", "organic"],
2275
+ loop: 4,
2276
+ source: glsl(
2277
+ `// Ink bleed: To spreads through From along an fbm threshold grown from seeds, with a wet, darkened fringe.
2278
+ ${pair(SAMPLE_IMAGES.city, SAMPLE_IMAGES.landscape)}
2279
+ uniform float uScale; // @control min=0.5 max=8 default=3 label="Scale"
2280
+ uniform float uEdge; // @control min=0.005 max=0.2 default=0.05 label="Edge width"
2281
+ uniform vec3 uInk; // @control color default=#1B1F3B label="Ink"`,
2282
+ HASH,
2283
+ NOISE,
2284
+ FBM,
2285
+ PICTURE,
2286
+ COMMON,
2287
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2288
+ vec2 uv = fragCoord / iResolution.xy;
2289
+ float t = progress();
2290
+ vec2 q = fragCoord / iResolution.y;
2291
+ // Distance to the nearest of four seeds spread across the frame, measured through a noisy warp so the blots are ragged.
2292
+ vec2 warped = q + (vec2(fbm(q * uScale, 5), fbm(q * uScale + 31.7, 5)) - 0.5) * 0.35;
2293
+ float aspect = iResolution.x / iResolution.y;
2294
+ float near = 1e9;
2295
+ for (int i = 0; i < 4; i++) {
2296
+ vec2 h = hash22(vec2(float(i), 3.7));
2297
+ vec2 seed = vec2((float(i) + 0.2 + 0.6 * h.x) / 4.0 * aspect, 0.2 + 0.6 * h.y);
2298
+ near = min(near, length(warped - seed));
2299
+ }
2300
+ // Fine noise adds capillary fingers to the front.
2301
+ float v = clamp(near / 0.5 * 0.75 + 0.25 * fbm(q * uScale * 4.0, 4) + 0.06 * (vnoise(q * uScale * 18.0) - 0.5), 0.0, 1.0);
2302
+ float front = mix(-2.0 * uEdge - 0.02, 1.0 + 2.0 * uEdge + 0.02, t);
2303
+ float gap = front - v;
2304
+ float wet = smoothstep(-uEdge * 0.3, uEdge * 0.3, gap);
2305
+ // Paper just ahead of the front darkens as it soaks; pigment pools just behind it.
2306
+ float soak = smoothstep(-2.0 * uEdge, 0.0, gap) * (1.0 - wet);
2307
+ float pool = wet * smoothstep(uEdge, 0.0, gap);
2308
+ vec2 bleed = vec2(soak - pool) * 0.004;
2309
+ vec3 a = cover(uFrom, uv + bleed);
2310
+ vec3 b = cover(uTo, uv - bleed);
2311
+ vec3 col = mix(a, b, wet);
2312
+ col = mix(col, col * 0.82, soak * 0.6);
2313
+ col = mix(col, uInk, pool * 0.85);
2314
+ fragColor = vec4(col, 1.0);
2315
+ }`
2316
+ ),
2317
+ presets: [
2318
+ { name: "Sepia wash", values: { uInk: [0.36, 0.23, 0.12], uEdge: 0.08, uScale: 2 } },
2319
+ { name: "Crisp", values: { uEdge: 0.015, uScale: 5 } }
2320
+ ]
2321
+ };
2322
+ var morphShaders = [
2323
+ featureMorph,
2324
+ silhouetteMorph,
2325
+ transitionLiquidMorph,
2326
+ transitionDisplacement,
2327
+ transitionLuma,
2328
+ transitionParticleSwap,
2329
+ transitionInkBleed
2330
+ ];
2331
+
1441
2332
  // src/catalog/shaders/overlay.ts
1442
2333
  var GLOW = `
1443
2334
  vec4 glow(vec3 c) {
@@ -2814,7 +3705,7 @@ var imageTreatments = [
2814
3705
  ];
2815
3706
 
2816
3707
  // src/catalog/text.ts
2817
- var r1 = (n2) => Math.round(n2 * 10) / 10;
3708
+ var r1 = (n3) => Math.round(n3 * 10) / 10;
2818
3709
  function fit(text, width, max, ems = textWidthEm(text)) {
2819
3710
  const e = Math.max(0.6, ems);
2820
3711
  const size = Math.min(max, width / e);
@@ -2827,7 +3718,7 @@ var mixHex = (a, b, t) => {
2827
3718
  return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
2828
3719
  };
2829
3720
  var holds = (id, attr, keys) => keys.map(([delay, value]) => ({ id, attrs: { [attr]: [value, value] }, delay, ms: 0 }));
2830
- var n = (name) => `{{${name}}}`;
3721
+ var n2 = (name) => `{{${name}}}`;
2831
3722
  var MONO = `ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
2832
3723
  var MONO_EM = 0.6;
2833
3724
  var HEAVY = `'Arial Black', 'Helvetica Neue', Impact, system-ui, sans-serif`;
@@ -2863,9 +3754,9 @@ var typewriter = {
2863
3754
  motion: {
2864
3755
  svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Hello, world.' label='Text'}} {{paper: color default=#111827 label='Background'}} {{ink: color default=#F9FAFB label='Text colour'}} {{pace: float min=30 max=300 default=90 step=5 label='Ms per letter'}} --><rect width="400" height="200" fill="{{paper}}"/><text id="typed" x="{{left}}" y="{{base}}" font-family="${MONO}" font-size="{{size}}" fill="{{ink}}" xml:space="preserve">{{text}}</text><text id="caret" x="{{end}}" y="{{base}}" font-family="${MONO}" font-size="{{size}}" fill="{{ink}}" opacity="0">\u258C</text></svg>`,
2865
3756
  animate: [
2866
- { id: "typed", write: "{{text}}", caret: "\u258C", ms: n("typing"), ease: "linear" },
3757
+ { id: "typed", write: "{{text}}", caret: "\u258C", ms: n2("typing"), ease: "linear" },
2867
3758
  // Hidden while the writer shows its own caret, then blinking where it stopped.
2868
- ...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n(`blink${k}`), k % 2 ? 0 : 1])])
3759
+ ...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n2(`blink${k}`), k % 2 ? 0 : 1])])
2869
3760
  ]
2870
3761
  }
2871
3762
  };
@@ -2892,12 +3783,12 @@ var scrambleText = {
2892
3783
  svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='ACCESS GRANTED' label='Text'}} {{glyphs: text default='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+' label='Scramble glyphs'}} {{background: color default=#05070D label='Background'}} {{ink: color default=#E2E8F0 label='Text colour'}} {{accent: color default=#22D3EE label='Accent'}} {{pace: float min=20 max=250 default=70 step=5 label='Ms per letter'}} {{glow: float min=0 max=6 default=2 step=0.5 label='Glow'}} --><defs><filter id="glow" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="{{glowWide}}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect width="400" height="200" fill="{{background}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" font-family="${MONO}" font-size="{{size}}" font-weight="700" fill="{{ink}}" filter="url(#glow)" xml:space="preserve">{{text}}</text><rect id="bar" x="{{barX}}" y="{{barY}}" width="0" height="3" fill="{{accent}}"/></svg>`,
2893
3784
  animate: [
2894
3785
  // Pure noise for a moment (the scramble runs before the change starts), then it decodes.
2895
- { id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n("decode"), ease: "linear" },
2896
- { id: "bar", attrs: { width: [0, n("barW")] }, delay: 500, ms: n("decode"), ease: "linear" }
3786
+ { id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n2("decode"), ease: "linear" },
3787
+ { id: "bar", attrs: { width: [0, n2("barW")] }, delay: 500, ms: n2("decode"), ease: "linear" }
2897
3788
  ]
2898
3789
  }
2899
3790
  };
2900
- var SLOTS = 20;
3791
+ var SLOTS2 = 20;
2901
3792
  var kineticStagger = {
2902
3793
  kind: "vector",
2903
3794
  id: "kinetic-stagger",
@@ -2912,7 +3803,7 @@ var kineticStagger = {
2912
3803
  ],
2913
3804
  // Up to SLOTS letters, each centred in its estimated advance; unused slots stay empty.
2914
3805
  derived: (v) => {
2915
- const letters = lettersOf(v.text).slice(0, SLOTS);
3806
+ const letters = lettersOf(v.text).slice(0, SLOTS2);
2916
3807
  const widths = letters.map((c) => textWidthEm(c));
2917
3808
  const ems = Math.max(0.6, widths.reduce((a, b) => a + b, 0));
2918
3809
  const size = Math.min(92, 370 / ems);
@@ -2929,7 +3820,7 @@ var kineticStagger = {
2929
3820
  lineAt: Math.round(letters.length * stagger + 400)
2930
3821
  };
2931
3822
  let x = 200 - ems * size / 2;
2932
- for (let i = 0; i < SLOTS; i++) {
3823
+ for (let i = 0; i < SLOTS2; i++) {
2933
3824
  const w = (widths[i] ?? 0) * size;
2934
3825
  out[`c${i}`] = letters[i]?.trim() ?? "";
2935
3826
  out[`x${i}`] = r1(x + w / 2);
@@ -2941,21 +3832,21 @@ var kineticStagger = {
2941
3832
  },
2942
3833
  motion: {
2943
3834
  svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Kinetic' label='Text'}} {{background: color default=#F8FAFC label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#F43F5E label='Underline'}} {{stagger: float min=20 max=200 default=55 step=5 label='Ms between letters'}} --><rect width="400" height="200" fill="{{background}}"/>${Array.from(
2944
- { length: SLOTS },
3835
+ { length: SLOTS2 },
2945
3836
  (_, i) => `<text id="l${i}" text-anchor="middle" transform="translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)" opacity="0" font-family="${HEAVY}" font-size="{{size}}" font-weight="900" fill="{{ink}}">{{c${i}}}</text>`
2946
3837
  ).join("")}<rect id="line" x="{{lineX}}" y="{{lineY}}" width="0" height="6" rx="3" fill="{{accent}}"/></svg>`,
2947
3838
  animate: [
2948
- ...Array.from({ length: SLOTS }, (_, i) => [
3839
+ ...Array.from({ length: SLOTS2 }, (_, i) => [
2949
3840
  {
2950
3841
  id: `l${i}`,
2951
3842
  attrs: { transform: [`translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)`, `translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`], opacity: [0, 1] },
2952
- delay: n(`d${i}`),
3843
+ delay: n2(`d${i}`),
2953
3844
  ms: 380,
2954
3845
  ease: "out"
2955
3846
  },
2956
- { id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n(`e${i}`), ms: 260, ease: "in-out" }
3847
+ { id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n2(`e${i}`), ms: 260, ease: "in-out" }
2957
3848
  ]).flat(),
2958
- { id: "line", attrs: { width: [0, n("lineW")] }, delay: n("lineAt"), ms: 500, ease: "in-out" }
3849
+ { id: "line", attrs: { width: [0, n2("lineW")] }, delay: n2("lineAt"), ms: 500, ease: "in-out" }
2959
3850
  ]
2960
3851
  }
2961
3852
  };
@@ -3024,7 +3915,7 @@ var strokeDrawText = {
3024
3915
  svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Draw' label='Text'}} {{background: color default=#FFFBF5 label='Background'}} {{outline: color default=#1E293B label='Outline'}} {{fill: color default=#F97316 label='Fill'}} {{width: float min=0.5 max=6 default=2 step=0.5 label='Outline width'}} {{fillOpacity: float min=0 max=1 default=1 label='Fill opacity'}} --><rect width="400" height="200" fill="{{background}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${HEAVY}" font-size="{{size}}" font-weight="900" fill="{{fill}}" fill-opacity="0" stroke="{{outline}}" stroke-width="{{width}}" stroke-linejoin="round" stroke-dasharray="0 {{dash}}">{{text}}</text></svg>`,
3025
3916
  animate: [
3026
3917
  { id: "word", attrs: { "stroke-dasharray": ["0 {{dash}}", "{{dash}} 0"] }, ms: 2e3, ease: "in-out" },
3027
- { id: "word", attrs: { "fill-opacity": [0, n("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
3918
+ { id: "word", attrs: { "fill-opacity": [0, n2("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
3028
3919
  ]
3029
3920
  }
3030
3921
  };
@@ -3161,7 +4052,7 @@ var wordCycle = {
3161
4052
  const at = { delay: (i + 1) * SHOW_MS + i * ROLL_MS, ms: ROLL_MS, ease: "in-out" };
3162
4053
  return [
3163
4054
  { id: "reel", attrs: { transform: [`translate(0 -{{o${i}}})`, `translate(0 -{{o${i + 1}}})`] }, ...at },
3164
- { id: "under", attrs: { width: [n(`len${i}`), n(`len${i + 1}`)] }, ...at }
4055
+ { id: "under", attrs: { width: [n2(`len${i}`), n2(`len${i + 1}`)] }, ...at }
3165
4056
  ];
3166
4057
  }).flat()
3167
4058
  }
@@ -3341,6 +4232,7 @@ var EFFECT_CATEGORIES = [
3341
4232
  "image",
3342
4233
  "overlay",
3343
4234
  "text",
4235
+ "morph",
3344
4236
  "transition",
3345
4237
  "loader",
3346
4238
  "data",
@@ -4022,9 +4914,10 @@ var SHADER_EFFECTS = [
4022
4914
  filmGrain,
4023
4915
  ...overlayEffects,
4024
4916
  ...textShaders,
4917
+ ...morphShaders,
4025
4918
  ...transitions
4026
4919
  ];
4027
- var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects].sort(
4920
+ var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects].sort(
4028
4921
  (a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
4029
4922
  );
4030
4923
  var byId = new Map(EFFECTS.map((e) => [e.id, e]));
@@ -4077,4 +4970,4 @@ export {
4077
4970
  effectControls,
4078
4971
  effectDefaults
4079
4972
  };
4080
- //# sourceMappingURL=chunk-FHXOMIQY.js.map
4973
+ //# sourceMappingURL=chunk-F754SFRY.js.map