@pixerate/flair 0.25.0 → 0.27.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 (33) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +10 -9
  3. package/dist/catalog/index.js +1 -1
  4. package/dist/{chunk-WAMA45G3.js → chunk-5USSDEQL.js} +77 -21
  5. package/dist/chunk-5USSDEQL.js.map +1 -0
  6. package/dist/{chunk-GTIAOYOK.js → chunk-74ZLCZOG.js} +13 -10
  7. package/dist/chunk-74ZLCZOG.js.map +1 -0
  8. package/dist/{chunk-O44ZJTGG.js → chunk-CK6EV37I.js} +15 -11
  9. package/dist/chunk-CK6EV37I.js.map +1 -0
  10. package/dist/{chunk-PYDN5G2P.js → chunk-DBNSI4MF.js} +1029 -8
  11. package/dist/chunk-DBNSI4MF.js.map +1 -0
  12. package/dist/{chunk-S2RGHHKZ.js → chunk-FJ5ZJGOL.js} +2 -2
  13. package/dist/{chunk-OKYMKSXK.js → chunk-PFUTUR7T.js} +3 -3
  14. package/dist/cli.js +9 -9
  15. package/dist/editor/index.js +1 -1
  16. package/dist/{effect-26CPF5HS.js → effect-M5T4ICIP.js} +4 -4
  17. package/dist/embed/index.js +2 -2
  18. package/dist/{export-DH2HMACO.js → export-QVW474KM.js} +3 -3
  19. package/dist/index.d.ts +42 -2
  20. package/dist/index.js +18 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/node.js +3 -3
  23. package/dist/react/index.js +2 -2
  24. package/dist/svelte/index.js +2 -2
  25. package/package.json +1 -1
  26. package/dist/chunk-GTIAOYOK.js.map +0 -1
  27. package/dist/chunk-O44ZJTGG.js.map +0 -1
  28. package/dist/chunk-PYDN5G2P.js.map +0 -1
  29. package/dist/chunk-WAMA45G3.js.map +0 -1
  30. /package/dist/{chunk-S2RGHHKZ.js.map → chunk-FJ5ZJGOL.js.map} +0 -0
  31. /package/dist/{chunk-OKYMKSXK.js.map → chunk-PFUTUR7T.js.map} +0 -0
  32. /package/dist/{effect-26CPF5HS.js.map → effect-M5T4ICIP.js.map} +0 -0
  33. /package/dist/{export-DH2HMACO.js.map → export-QVW474KM.js.map} +0 -0
@@ -1558,6 +1558,167 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1558
1558
  { name: "Dense", values: { uLanes: 26, uDensity: 1, uWidth: 0.12, uPulses: 0.8 } }
1559
1559
  ]
1560
1560
  };
1561
+ var memphisPattern = {
1562
+ kind: "shader",
1563
+ id: "memphis-pattern",
1564
+ name: "Memphis pattern",
1565
+ description: "An 80s Memphis-design field of squiggles, zigzags, triangles, rings, dots and confetti, bobbing, drifting and boiling like hand-drawn ink.",
1566
+ category: "pattern",
1567
+ tags: ["background", "retro", "80s", "playful", "confetti"],
1568
+ loop: 8,
1569
+ source: glsl(
1570
+ `// Memphis pattern: scattered flat-colour shapes that bob, drift and boil.
1571
+ uniform vec3 uBackground; // @control color default=#FFF3DC label="Background"
1572
+ uniform vec3 uInkA; // @control color default=#FF4FA3 label="Ink A"
1573
+ uniform vec3 uInkB; // @control color default=#00B3C7 label="Ink B"
1574
+ uniform vec3 uInkC; // @control color default=#FFC23C label="Ink C"
1575
+ uniform vec3 uInkD; // @control color default=#1B1B1B label="Ink D"
1576
+ uniform float uDensity; // @control min=0 max=1 default=0.7 label="Density"
1577
+ uniform float uScale; // @control min=2 max=20 default=7 label="Shapes across"
1578
+ uniform float uDrift; // @control min=0 max=1 default=0.3 label="Drift"
1579
+ uniform float uWobble; // @control min=0 max=1 default=0.5 label="Wobble"
1580
+ uniform float uWeight; // @control min=0.02 max=0.16 default=0.07 label="Line weight"`,
1581
+ HASH,
1582
+ NOISE,
1583
+ ROTATE,
1584
+ `#define TAU 6.28318
1585
+ #define LOOP 8.0
1586
+
1587
+ float segDist(vec2 p, vec2 a, vec2 b) {
1588
+ vec2 pa = p - a;
1589
+ vec2 ba = b - a;
1590
+ float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
1591
+ return length(pa - ba * h);
1592
+ }
1593
+
1594
+ float triDist(vec2 p, float r) {
1595
+ const float k = 1.7320508;
1596
+ p.x = abs(p.x) - r;
1597
+ p.y = p.y + r / k;
1598
+ if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
1599
+ p.x -= clamp(p.x, -2.0 * r, 0.0);
1600
+ return -length(p) * sign(p.y);
1601
+ }
1602
+
1603
+ // Distance to a sine (or zigzag) stroke centred on the origin, halfLen long.
1604
+ float waveDist(vec2 q, float amp, float freq, bool zig, float halfLen) {
1605
+ float x = clamp(q.x, -halfLen, halfLen);
1606
+ float y;
1607
+ float slope;
1608
+ if (zig) {
1609
+ float f = fract(x * freq + 0.25);
1610
+ y = amp * (abs(f - 0.5) * 4.0 - 1.0);
1611
+ slope = 4.0 * amp * freq;
1612
+ } else {
1613
+ y = amp * sin(x * freq * TAU);
1614
+ slope = amp * freq * TAU * cos(x * freq * TAU);
1615
+ }
1616
+ return length(vec2(q.x - x, (q.y - y) / sqrt(1.0 + slope * slope)));
1617
+ }
1618
+
1619
+ float shapeDist(int kind, vec2 q, float w) {
1620
+ if (kind == 0) return waveDist(q, 0.08, 2.2, false, 0.36) - w;
1621
+ if (kind == 1) return waveDist(q, 0.08, 2.5, true, 0.34) - w;
1622
+ if (kind == 2) return triDist(q, 0.2);
1623
+ if (kind == 3) return abs(length(q) - 0.16) - w;
1624
+ if (kind == 4) return length(q) - 0.1;
1625
+ if (kind == 5) return segDist(q, vec2(-0.13, 0.0), vec2(0.13, 0.0)) - w;
1626
+ vec2 g = q - clamp(round(q / 0.11), -1.0, 1.0) * 0.11;
1627
+ return length(g) - 0.028;
1628
+ }
1629
+
1630
+ vec3 ink(float h) {
1631
+ if (h < 0.25) return uInkA;
1632
+ if (h < 0.5) return uInkB;
1633
+ if (h < 0.75) return uInkC;
1634
+ return uInkD;
1635
+ }
1636
+
1637
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1638
+ float px = uScale / iResolution.y;
1639
+ float t = TAU * iTime / LOOP;
1640
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
1641
+ p += uDrift * vec2(2.0 * cos(t), sin(2.0 * t));
1642
+ // Boil: the line wobble jumps six times a second, like redrawn frames.
1643
+ float frame = mod(floor(iTime * 6.0), LOOP * 6.0);
1644
+ vec2 n = vec2(vnoise(p * 3.0 + frame * 7.13), vnoise(p * 3.0 + frame * 3.71 + 19.0)) - 0.5;
1645
+ p += n * uWobble * 0.07;
1646
+
1647
+ vec3 col = uBackground;
1648
+ vec2 id0 = floor(p);
1649
+ for (int j = -1; j <= 1; j++) {
1650
+ for (int i = -1; i <= 1; i++) {
1651
+ vec2 id = id0 + vec2(float(i), float(j));
1652
+ if (hash12(id) > uDensity) continue;
1653
+ float ph = hash12(id + 11.1) * TAU;
1654
+ vec2 c = id + 0.5 + (hash22(id + 3.7) - 0.5) * 0.6;
1655
+ c.y += 0.06 * sin(t * 2.0 + ph);
1656
+ float ang = hash12(id + 5.3) * TAU + 0.35 * sin(t + ph);
1657
+ float sz = 0.8 + 0.5 * hash12(id + 8.8);
1658
+ vec2 q = rot(ang) * (p - c) / sz;
1659
+ int kind = min(int(hash12(id + 2.2) * 7.0), 6);
1660
+ float d = shapeDist(kind, q, uWeight * 0.5 / sz) * sz;
1661
+ float cover = 1.0 - smoothstep(-0.75 * px, 0.75 * px, d);
1662
+ col = mix(col, ink(hash12(id + 9.9)), cover);
1663
+ }
1664
+ }
1665
+ fragColor = vec4(col, 1.0);
1666
+ }`
1667
+ ),
1668
+ presets: [
1669
+ { name: "Saved by the bell", values: {} },
1670
+ { name: "Pastel", values: { uBackground: [0.97, 0.95, 1], uInkA: [1, 0.7, 0.8], uInkB: [0.62, 0.85, 0.95], uInkC: [1, 0.87, 0.55], uInkD: [0.72, 0.65, 0.95], uWobble: 0.3 } },
1671
+ { name: "After dark", values: { uBackground: [0.07, 0.06, 0.14], uInkA: [1, 0.25, 0.65], uInkB: [0.15, 0.95, 0.9], uInkC: [1, 0.85, 0.2], uInkD: [0.98, 0.98, 0.98] } },
1672
+ { name: "Sparse", values: { uDensity: 0.35, uScale: 5, uDrift: 0.15, uWeight: 0.05 } }
1673
+ ]
1674
+ };
1675
+ var chevronTunnel = {
1676
+ kind: "shader",
1677
+ id: "chevron-tunnel",
1678
+ name: "Chevron tunnel",
1679
+ description: "Two-tone hazard chevrons radiating from a centre point and streaming outward like a tunnel, behind a title card.",
1680
+ category: "pattern",
1681
+ tags: ["background", "hazard", "warning", "tunnel", "title", "loop"],
1682
+ loop: 1,
1683
+ source: glsl(
1684
+ `// Chevron tunnel: hazard chevrons streaming out of (or into) a centre point.
1685
+ uniform vec3 uColorA; // @control color default=#FACC15 label="Colour A"
1686
+ uniform vec3 uColorB; // @control color default=#111111 label="Colour B"
1687
+ uniform int uCount; // @control min=2 max=16 default=8 label="Chevrons around"
1688
+ uniform float uFrequency; // @control min=0.5 max=12 default=3 label="Stripe frequency"
1689
+ uniform float uSpeed; // @control min=0 max=4 default=1 label="Speed"
1690
+ uniform bool uInward; // @control default=false label="Stream inward"
1691
+ uniform vec2 uCentre; // @control min=0 max=1 default=0.5,0.5 label="Centre"
1692
+ uniform float uAngle; // @control min=-180 max=180 default=0 label="Angle (\xB0)"
1693
+ uniform float uTwist; // @control min=-3 max=3 default=0 label="Twist"
1694
+ uniform float uDepth; // @control min=0 max=3 default=1 label="Chevron point"
1695
+ uniform float uSoft; // @control min=0 max=1 default=0 label="Softness"
1696
+ uniform float uVignette; // @control min=0 max=1 default=0.35 label="Vignette"`,
1697
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
1698
+ vec2 p = (fragCoord - uCentre * iResolution.xy) / iResolution.y;
1699
+ float lr = log(max(length(p), 1e-4));
1700
+ float a = atan(p.y, p.x) - radians(uAngle) - uTwist * lr;
1701
+ // 0 down the middle of each chevron, 1 at its edges.
1702
+ float side = abs(fract(a * float(uCount) / 6.28318) - 0.5) * 2.0;
1703
+ float dir = uInward ? 1.0 : -1.0;
1704
+ // Log radius gives tunnel perspective; chevrons point the way they travel.
1705
+ float s = lr * uFrequency - dir * uDepth * side + dir * iTime * uSpeed;
1706
+ float tri = abs(fract(s) - 0.5);
1707
+ float e = max(fwidth(s) * 0.75, uSoft * 0.25) + 1e-4;
1708
+ float m = smoothstep(0.25 - e, 0.25 + e, tri);
1709
+ vec3 col = mix(uColorA, uColorB, m);
1710
+ vec2 uv = fragCoord / iResolution.xy - 0.5;
1711
+ col *= 1.0 - uVignette * smoothstep(0.1, 0.75, dot(uv, uv) * 2.0);
1712
+ fragColor = vec4(col, 1.0);
1713
+ }`
1714
+ ),
1715
+ presets: [
1716
+ { name: "Hazard", values: {} },
1717
+ { name: "Red alert", values: { uColorA: [0.95, 0.12, 0.15], uColorB: [0.98, 0.96, 0.92], uCount: 6, uSpeed: 1.5, uVignette: 0.5 } },
1718
+ { name: "Rave warp", values: { uColorA: [1, 0.2, 0.7], uColorB: [0.1, 0.9, 1], uTwist: 1.2, uFrequency: 4, uSpeed: 2, uCount: 10 } },
1719
+ { name: "Suck in", values: { uInward: true, uDepth: 1.6, uCount: 4, uSoft: 0.1 } }
1720
+ ]
1721
+ };
1561
1722
 
1562
1723
  // src/catalog/interaction.ts
1563
1724
  var r1 = (v) => Math.round(v * 10) / 10;
@@ -2065,6 +2226,113 @@ var pressButton = {
2065
2226
  ]
2066
2227
  }
2067
2228
  };
2229
+ var TOAST = { x: 92, y: 24, w: 252, h: 68 };
2230
+ var TOAST_OFF = "translate(300 0)";
2231
+ var TOAST_IN = 520;
2232
+ var toastAt = (x) => `translate(${x} 0)`;
2233
+ var BADGE_X = TOAST.x + 32;
2234
+ var BADGE_Y = TOAST.y + 34;
2235
+ var toastNotification = {
2236
+ kind: "vector",
2237
+ id: "toast-notification",
2238
+ name: "Toast notification",
2239
+ description: "A toast card that slides in from the top right with a slight overshoot, draws a tick in its badge as its timer bar runs down, then slides away by itself.",
2240
+ category: "interaction",
2241
+ tags: ["interactive", "ui", "click", "toast", "notification", "alert", "success", "feedback", "micro-interaction"],
2242
+ trigger: "click",
2243
+ hold: 600,
2244
+ presets: [
2245
+ { name: "Light", values: { background: [0.96, 0.97, 0.98], card: [1, 1, 1], ink: [0.07, 0.09, 0.15], accent: [0.09, 0.64, 0.29] } },
2246
+ { name: "Indigo", values: { title: "Deploy complete", message: "Live on production in 42s", accent: [0.39, 0.4, 0.95] } }
2247
+ ],
2248
+ derived: (v) => {
2249
+ const title = fit(v.title, 164, 15);
2250
+ const message = fit(v.message, 164, 12.5);
2251
+ const stay = Math.round(Math.min(5, Math.max(1, Number(v.duration) || 2.4)) * 1e3);
2252
+ const leave = TOAST_IN + stay;
2253
+ return { titleSize: title.size, titleLen: title.length, messageSize: message.size, messageLen: message.length, stay, leave, nudged: leave + 120, fade: leave + 300, gone: leave + 520 };
2254
+ },
2255
+ motion: {
2256
+ svg: `<svg viewBox="0 0 360 200"><!-- {{title: text default='Key generated' label='Title'}} {{message: text default='Ready for remote calls' label='Message'}} {{duration: float min=1 max=5 default=2.4 step=0.1 label='Time on screen (s)'}} {{background: color default=#0B0F17 label='Background'}} {{card: color default=#171C27 label='Card'}} {{accent: color default=#22C55E label='Accent'}} {{ink: color default=#F3F4F6 label='Text colour'}} --><defs><clipPath id="toastClip"><rect x="${TOAST.x}" y="${TOAST.y}" width="${TOAST.w}" height="${TOAST.h}" rx="14"/></clipPath><filter id="toastShadow" x="-20%" y="-40%" width="140%" height="180%"><feGaussianBlur stdDeviation="8"/></filter></defs><rect width="360" height="200" fill="{{background}}"/><g fill="{{ink}}" fill-opacity="0.07"><circle cx="34" cy="34" r="9"/><rect x="50" y="29" width="64" height="10" rx="5"/><rect x="24" y="118" width="140" height="12" rx="6"/><rect x="24" y="142" width="312" height="8" rx="4"/><rect x="24" y="158" width="282" height="8" rx="4"/><rect x="24" y="174" width="220" height="8" rx="4"/></g><g id="toast" transform="${TOAST_OFF}" opacity="0"><rect x="${TOAST.x}" y="${TOAST.y + 8}" width="${TOAST.w}" height="${TOAST.h}" rx="14" fill="#000000" opacity="0.3" filter="url(#toastShadow)"/><rect x="${TOAST.x}" y="${TOAST.y}" width="${TOAST.w}" height="${TOAST.h}" rx="14" fill="{{card}}" stroke="{{ink}}" stroke-opacity="0.08"/><g id="badge" transform="${about(BADGE_X, BADGE_Y, 0)}"><circle id="halo" cx="${BADGE_X}" cy="${BADGE_Y}" r="15" fill="{{accent}}" opacity="0"/><circle cx="${BADGE_X}" cy="${BADGE_Y}" r="15" fill="{{accent}}"/><path id="tick" d="M${BADGE_X - 6.5} ${BADGE_Y + 0.5} L${BADGE_X - 2} ${BADGE_Y + 5} L${BADGE_X + 6.5} ${BADGE_Y - 4.5}" fill="none" stroke="{{card}}" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g><text x="${TOAST.x + 58}" y="${TOAST.y + 28}" textLength="{{titleLen}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{titleSize}}" font-weight="650" fill="{{ink}}">{{title}}</text><text x="${TOAST.x + 58}" y="${TOAST.y + 47}" textLength="{{messageLen}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{messageSize}}" font-weight="400" fill="{{ink}}" fill-opacity="0.62">{{message}}</text><path d="M324 36 L332 44 M332 36 L324 44" stroke="{{ink}}" stroke-opacity="0.35" stroke-width="1.6" stroke-linecap="round"/><rect id="timer" x="${TOAST.x}" y="${TOAST.y + TOAST.h - 4}" width="${TOAST.w}" height="4" fill="{{accent}}" clip-path="url(#toastClip)"/></g></svg>`,
2257
+ animate: [
2258
+ // One step that returns by itself: in past its place and back, a while on screen, then away right.
2259
+ { id: "toast", step: 1, attrs: { transform: [TOAST_OFF, toastAt(-9)] }, ms: 340, ease: "out" },
2260
+ { id: "toast", step: 1, attrs: { transform: [toastAt(-9), toastAt(0)] }, delay: 340, ms: 200, ease: "in-out" },
2261
+ { id: "toast", step: 1, attrs: { opacity: [0, 1] }, ms: 160 },
2262
+ // The badge pops, its tick draws on and a soft ring breathes out.
2263
+ { id: "badge", step: 1, attrs: { transform: [about(BADGE_X, BADGE_Y, 0), about(BADGE_X, BADGE_Y, 1.18)] }, delay: 220, ms: 200, ease: "out" },
2264
+ { id: "badge", step: 1, attrs: { transform: [about(BADGE_X, BADGE_Y, 1.18), about(BADGE_X, BADGE_Y, 1)] }, delay: 420, ms: 180, ease: "in-out" },
2265
+ { id: "tick", step: 1, attrs: { opacity: [0, 1] }, delay: 400, ms: 40 },
2266
+ { id: "tick", step: 1, draw: true, delay: 400, ms: 300, ease: "out" },
2267
+ { id: "halo", step: 1, attrs: { r: [15, 26], opacity: [0.45, 0] }, delay: 440, ms: 650, ease: "out" },
2268
+ // The timer runs down, then the card nudges left and slides off.
2269
+ { id: "timer", step: 1, attrs: { width: [TOAST.w, 0] }, delay: TOAST_IN, ms: n("stay"), ease: "linear" },
2270
+ { id: "toast", step: 1, attrs: { transform: [toastAt(0), toastAt(-6)] }, delay: n("leave"), ms: 120, ease: "out" },
2271
+ { id: "toast", step: 1, attrs: { transform: [toastAt(-6), TOAST_OFF] }, delay: n("nudged"), ms: 380, ease: "in-out" },
2272
+ { id: "toast", step: 1, attrs: { opacity: [1, 0] }, delay: n("fade"), ms: 200 },
2273
+ // Out of sight, everything it flashed goes back for the next round.
2274
+ snap("badge", "transform", about(BADGE_X, BADGE_Y, 0), n("gone")),
2275
+ snap("tick", "opacity", 0, n("gone")),
2276
+ { id: "tick", step: 1, draw: [0, 0], delay: n("gone"), ms: 0 },
2277
+ snap("halo", "r", 15, n("gone")),
2278
+ snap("halo", "opacity", 0.45, n("gone")),
2279
+ snap("timer", "width", TOAST.w, n("gone"))
2280
+ ]
2281
+ }
2282
+ };
2283
+ var SLIDER = { x: 40, y: 90, w: 280 };
2284
+ var KNOB_FROM = r1(SLIDER.x + SLIDER.w * 0.14);
2285
+ var KNOB_TO = r1(SLIDER.x + SLIDER.w * 0.82);
2286
+ var knobAt = (x) => `translate(${r1(x)} ${SLIDER.y})`;
2287
+ var cursorAt = (x, y = SLIDER.y + 3) => `translate(${r1(x)} ${y})`;
2288
+ var CURSOR_REST = cursorAt(304, 138);
2289
+ var SCRUB_BACK = 2800;
2290
+ var sliderScrub = {
2291
+ kind: "vector",
2292
+ id: "slider-scrub",
2293
+ name: "Slider scrub",
2294
+ description: "A labelled slider whose knob a cursor grabs and drags along the track, the fill and value readout counting with it, settling with a small bounce before it glides back.",
2295
+ category: "interaction",
2296
+ tags: ["interactive", "ui", "click", "slider", "range", "input", "settings", "cursor", "micro-interaction"],
2297
+ trigger: "click",
2298
+ hold: 600,
2299
+ presets: [
2300
+ { name: "Light", values: { background: [1, 1, 1], track: [0.9, 0.91, 0.94], ink: [0.07, 0.09, 0.15], accent: [0.31, 0.27, 0.9] } },
2301
+ { name: "Volume", values: { label: "Volume", from: 20, to: 85, accent: [0.96, 0.45, 0.2] } }
2302
+ ],
2303
+ derived: (v) => {
2304
+ const { size, length } = fit(v.label, 200, 16);
2305
+ return { size, length };
2306
+ },
2307
+ motion: (() => {
2308
+ const move = (from, to, delay, ms, withCursor) => [
2309
+ { id: "knob", step: 1, attrs: { transform: [knobAt(from), knobAt(to)] }, delay, ms, ease: "in-out" },
2310
+ { id: "fill", step: 1, attrs: { width: [r1(from - SLIDER.x), r1(to - SLIDER.x)] }, delay, ms, ease: "in-out" },
2311
+ ...withCursor ? [{ id: "cursor", step: 1, attrs: { transform: [cursorAt(from + 2), cursorAt(to + 2)] }, delay, ms, ease: "in-out" }] : []
2312
+ ];
2313
+ return {
2314
+ svg: `<svg viewBox="0 0 360 160"><!-- {{label: text default='Number of results' label='Label'}} {{from: int min=0 max=99999 default=50 label='From value'}} {{to: int min=0 max=99999 default=306 label='To value'}} {{background: color default=#0B0F17 label='Background'}} {{accent: color default=#8B5CF6 label='Accent'}} {{track: color default=#272D3A label='Track'}} {{ink: color default=#F3F4F6 label='Text colour'}} --><rect width="360" height="160" fill="{{background}}"/><text x="${SLIDER.x}" y="58" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{label}}</text><text id="value" x="${SLIDER.x + SLIDER.w}" y="58" text-anchor="end" font-family="${SANS}" font-size="16" font-weight="700" fill="{{accent}}">{{from}}</text><rect x="${SLIDER.x}" y="${SLIDER.y - 3}" width="${SLIDER.w}" height="6" rx="3" fill="{{track}}"/><rect id="fill" x="${SLIDER.x}" y="${SLIDER.y - 3}" width="${r1(KNOB_FROM - SLIDER.x)}" height="6" rx="3" fill="{{accent}}"/><g id="knob" transform="${knobAt(KNOB_FROM)}"><circle id="halo" r="11" fill="{{accent}}" opacity="0"/><circle cy="1.5" r="11" fill="#000000" opacity="0.2"/><circle id="dot" r="10" fill="#FFFFFF" stroke="{{accent}}" stroke-width="3"/></g><g id="cursor" transform="${CURSOR_REST}"><path d="M0 0 L0 17 L4.6 12.8 L7.8 19.6 L10.6 18.3 L7.5 11.7 L13.4 11.4 Z" fill="#FFFFFF" stroke="#111827" stroke-width="1.3" stroke-linejoin="round"/></g></svg>`,
2315
+ animate: [
2316
+ // One step that returns by itself: the cursor comes to the knob and grabs it.
2317
+ { id: "cursor", step: 1, attrs: { transform: [CURSOR_REST, cursorAt(KNOB_FROM + 2)] }, ms: 420, ease: "in-out" },
2318
+ { id: "dot", step: 1, attrs: { r: [10, 12] }, delay: 420, ms: 120, ease: "out" },
2319
+ { id: "halo", step: 1, attrs: { r: [11, 20], opacity: [0, 0.22] }, delay: 420, ms: 160, ease: "out" },
2320
+ // Dragged a touch past, let go, it springs back and settles as the value counts.
2321
+ ...move(KNOB_FROM, KNOB_TO + 7, 540, 1e3, true),
2322
+ { id: "value", step: 1, count: [n("from"), n("to")], delay: 540, ms: 1e3, ease: "in-out" },
2323
+ { id: "dot", step: 1, attrs: { r: [12, 10] }, delay: 1540, ms: 160, ease: "out" },
2324
+ { id: "halo", step: 1, attrs: { r: [20, 26], opacity: [0.22, 0] }, delay: 1540, ms: 260, ease: "out" },
2325
+ snap("halo", "r", 11, 1900),
2326
+ ...move(KNOB_TO + 7, KNOB_TO - 3, 1540, 170, false),
2327
+ ...move(KNOB_TO - 3, KNOB_TO, 1710, 150, false),
2328
+ // Back to where it began: the cursor leaves and the knob glides home.
2329
+ { id: "cursor", step: 1, attrs: { transform: [cursorAt(KNOB_TO + 9), CURSOR_REST] }, delay: SCRUB_BACK, ms: 450, ease: "in-out" },
2330
+ ...move(KNOB_TO, KNOB_FROM, SCRUB_BACK + 100, 600, false),
2331
+ { id: "value", step: 1, count: [n("to"), n("from")], delay: SCRUB_BACK + 100, ms: 600, ease: "in-out" }
2332
+ ]
2333
+ };
2334
+ })()
2335
+ };
2068
2336
  var interactionEffects = [
2069
2337
  likeButton,
2070
2338
  toggleSwitch,
@@ -2077,7 +2345,9 @@ var interactionEffects = [
2077
2345
  linkHover,
2078
2346
  buttonHoverFill,
2079
2347
  tooltipPop,
2080
- pressButton
2348
+ pressButton,
2349
+ toastNotification,
2350
+ sliderScrub
2081
2351
  ];
2082
2352
 
2083
2353
  // src/catalog/splatter.ts
@@ -2708,7 +2978,87 @@ var washiTape = {
2708
2978
  ]
2709
2979
  }
2710
2980
  };
2711
- var stickerEffects = [stickerSlap, stickerPeel, holoSticker, stickerSheet, stampBadge, washiTape];
2981
+ function sealOutline(points, sharp) {
2982
+ const count = Math.max(6, Math.round(Number(points)));
2983
+ if (sharp) return star(120, 120, 90, 79, count);
2984
+ const samples = Math.max(112, count * 8);
2985
+ return outline(
2986
+ Array.from({ length: samples }, (_, k) => {
2987
+ const a = k / samples * Math.PI * 2 - Math.PI / 2;
2988
+ const rr = 88 * (0.93 + 0.07 * Math.cos(a * count + Math.PI / 2 * count));
2989
+ return [120 + rr * Math.cos(a), 120 + rr * Math.sin(a)];
2990
+ })
2991
+ );
2992
+ }
2993
+ var SEAL_PULSES = [0.22, 0.68];
2994
+ var SEAL_BEAT = [
2995
+ [0, 1],
2996
+ [170, 1.12],
2997
+ [320, 0.96],
2998
+ [460, 1]
2999
+ ];
3000
+ var SEAL_GLINT_MS = 560;
3001
+ var sealBadge = {
3002
+ kind: "vector",
3003
+ id: "seal-badge",
3004
+ name: "Seal badge",
3005
+ description: 'A scalloped seal sticker (a "NEW" or "UPDATE LIVE" price-tag burst) turning slowly under upright one- or two-line text, pulsing now and then as a glint sweeps across its face.',
3006
+ category: "sticker",
3007
+ tags: ["sticker", "seal", "badge", "burst", "price-tag", "label", "spin", "new", "loop"],
3008
+ hold: 0,
3009
+ presets: [
3010
+ { name: "New", values: { top: "NEW", bottom: "", seal: [0.98, 0.8, 0.12], ink: [0.25, 0.13, 0.01], points: 16 } },
3011
+ { name: "Sale", values: { top: "50%", bottom: "OFF", seal: [0.93, 0.16, 0.24], sharp: true, points: 28, turn: 8 } },
3012
+ { name: "Mint", values: { top: "JUST", bottom: "LAUNCHED", seal: [0.2, 0.83, 0.6], ink: [0.03, 0.2, 0.14], background: [0.09, 0.1, 0.13], border: [0.95, 0.97, 0.96] } }
3013
+ ],
3014
+ derived: (v) => {
3015
+ const period = Math.round(Number(v.turn) * 1e3);
3016
+ const d = sealOutline(v.points, v.sharp);
3017
+ const two = String(v.bottom).trim() !== "";
3018
+ const top = fit2(v.top, two ? 118 : 124, two ? 36 : 46);
3019
+ const bottom = two ? fit2(v.bottom, 104, 30) : { size: 0, length: 0 };
3020
+ const gap = two ? 4 : 0;
3021
+ const height = top.size + gap + bottom.size;
3022
+ const topMid = 120 - height / 2 + top.size / 2;
3023
+ const bottomMid = topMid + top.size / 2 + gap + bottom.size / 2;
3024
+ const beats = Object.fromEntries(
3025
+ SEAL_PULSES.flatMap((f, i) => {
3026
+ const at3 = Math.round(period * f);
3027
+ return [...SEAL_BEAT.map(([ms], j) => [`beat${i}_${j}`, at3 + ms]), [`glint${i}`, at3 + 60]];
3028
+ })
3029
+ );
3030
+ return {
3031
+ period,
3032
+ d,
3033
+ topSize: top.size,
3034
+ topLength: top.length,
3035
+ topBase: r13(topMid + top.size * 0.36),
3036
+ bottomSize: Math.max(1, bottom.size),
3037
+ bottomLength: Math.max(1, bottom.length),
3038
+ bottomBase: r13(bottomMid + bottom.size * 0.36),
3039
+ ...beats
3040
+ };
3041
+ },
3042
+ motion: {
3043
+ svg: `<svg viewBox="0 0 240 240"><!-- {{top: text default='UPDATE' label='Top line'}} {{bottom: text default='LIVE' label='Bottom line (may be empty)'}} {{background: color default=#F3EFE6 label='Background'}} {{seal: color default=#FF3D6E label='Seal'}} {{ink: color default=#FFFFFF label='Text colour'}} {{border: color default=#FFFFFF label='Border'}} {{points: int min=8 max=40 default=20 label='Points'}} {{sharp: bool default=false label='Sharp points'}} {{turn: float min=3 max=40 default=14 step=0.5 label='Spin (seconds per turn)'}} ${FONT} --><defs><filter id="sealBadgeSoft" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="5"/></filter>${gloss("sealBadgeGloss")}<linearGradient id="sealBadgeGlintFill" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#FFFFFF" stop-opacity="0"/><stop offset="0.5" stop-color="#FFFFFF" stop-opacity="0.65"/><stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/></linearGradient><clipPath id="sealBadgeClip"><path id="sealClipShape" d="{{d}}" transform="rotate(0 120 120)"/></clipPath></defs><rect width="240" height="240" fill="{{background}}"/><g id="sealPulse" transform="${pose(120, 120, 1, 0)}"><circle cx="120" cy="127" r="92" fill="#000000" opacity="0.28" filter="url(#sealBadgeSoft)"/><g id="sealSpin" transform="rotate(0 120 120)"><path d="{{d}}" fill="{{border}}" stroke="{{border}}" stroke-width="10" stroke-linejoin="round"/><path d="{{d}}" fill="{{seal}}"/><circle cx="120" cy="120" r="70" fill="none" stroke="{{ink}}" stroke-opacity="0.6" stroke-width="2" stroke-dasharray="1.5 5.5" stroke-linecap="round"/><circle cx="120" cy="120" r="63" fill="none" stroke="{{ink}}" stroke-opacity="0.3" stroke-width="1.2"/></g><g clip-path="url(#sealBadgeClip)"><rect width="240" height="240" fill="url(#sealBadgeGloss)"/><rect id="sealGlint" x="0" y="20" width="40" height="200" fill="url(#sealBadgeGlintFill)" transform="translate(-14 0) skewX(-20)"/></g><g font-family="{{font}}" font-weight="900" fill="{{ink}}" text-anchor="middle"><text x="120" y="{{topBase}}" textLength="{{topLength}}" lengthAdjust="spacingAndGlyphs" font-size="{{topSize}}">{{top}}</text><text x="120" y="{{bottomBase}}" textLength="{{bottomLength}}" lengthAdjust="spacingAndGlyphs" font-size="{{bottomSize}}">{{bottom}}</text></g></g></svg>`,
3044
+ animate: [
3045
+ // One whole turn a loop, the clip turning with it so the shine stays on the seal.
3046
+ ...["sealSpin", "sealClipShape"].map((id) => ({ id, attrs: { transform: ["rotate(0 120 120)", "rotate(360 120 120)"] }, ms: n4("period"), ease: "linear" })),
3047
+ // Now and then it pulses and a glint sweeps across.
3048
+ ...SEAL_PULSES.flatMap((_, i) => [
3049
+ ...SEAL_BEAT.slice(1).map(([at3, scale], j) => ({
3050
+ id: "sealPulse",
3051
+ attrs: { transform: [pose(120, 120, SEAL_BEAT[j][1], 0), pose(120, 120, scale, 0)] },
3052
+ delay: n4(`beat${i}_${j}`),
3053
+ ms: at3 - SEAL_BEAT[j][0],
3054
+ ease: j === 0 ? "out" : "in-out"
3055
+ })),
3056
+ { id: "sealGlint", attrs: { transform: ["translate(-14 0) skewX(-20)", "translate(300 0) skewX(-20)"] }, delay: n4(`glint${i}`), ms: SEAL_GLINT_MS, ease: "in-out" }
3057
+ ])
3058
+ ]
3059
+ }
3060
+ };
3061
+ var stickerEffects = [stickerSlap, stickerPeel, holoSticker, stickerSheet, stampBadge, washiTape, sealBadge];
2712
3062
 
2713
3063
  // src/catalog/trail.ts
2714
3064
  var r14 = (v) => {
@@ -3567,7 +3917,91 @@ var lineBoil = {
3567
3917
  }))
3568
3918
  }
3569
3919
  };
3570
- var comicEffects = [soundEffect, speechBubble, fightCloud, dizzyStars, animeEmotes, cartoonPoof, lineBoil];
3920
+ var DB_X = 200;
3921
+ var DB_Y = 120;
3922
+ var DB_OUT = 2450;
3923
+ var DB_END = 2860;
3924
+ var DB_BOIL = 120;
3925
+ var DB_SEEDS = [3, 11, 19, 27];
3926
+ var DB_SHAPES = [
3927
+ `<path d="M0 -7 L6.5 5 L-6.5 5 Z"/>`,
3928
+ `<circle cx="0" cy="0" r="6"/>`,
3929
+ `<path d="M-7 0 H7"/>`,
3930
+ `<path d="M-11 1 q2.75 -7 5.5 0 t5.5 0 t5.5 0 t5.5 0"/>`,
3931
+ `<path d="M-6.5 0 H6.5 M0 -6.5 V6.5"/>`,
3932
+ `<path d="M-6 -3 H6 M-3.5 3.5 H3.5"/>`
3933
+ ];
3934
+ var DB_DOODLES = Array.from({ length: 14 }, (_, k) => {
3935
+ const a = -Math.PI / 2 + k / 14 * Math.PI * 2 + (rand(k + 200) - 0.5) * 0.3;
3936
+ const reach = 1 + rand(k + 210) * 0.1;
3937
+ return {
3938
+ a,
3939
+ x: DB_X + 152 * reach * Math.cos(a),
3940
+ y: DB_Y + 96 * reach * Math.sin(a),
3941
+ shape: (k * 7 + 3) % DB_SHAPES.length,
3942
+ colour: ["doodleA", "doodleB", "doodleC"][k * 5 % 14 % 3],
3943
+ turn: Math.round((rand(k + 220) - 0.5) * 80),
3944
+ at: 110 + k * 5 % 14 * 42
3945
+ };
3946
+ });
3947
+ var dbPose = (d, u, s, rot) => `translate(${r15(DB_X + (d.x - DB_X) * u)} ${r15(DB_Y + (d.y - DB_Y) * u)}) rotate(${r15(rot)}) scale(${r27(s)})`;
3948
+ var dbCard = (s, rot) => `translate(${DB_X} ${DB_Y}) rotate(${r15(rot)}) rotate({{tilt}}) scale(${r27(s)}) translate(${-DB_X} ${-DB_Y})`;
3949
+ var doodleBurst = {
3950
+ kind: "vector",
3951
+ id: "doodle-burst",
3952
+ name: "Doodle burst",
3953
+ description: "A tilted label card that pops in past its size while hand-drawn doodles (triangles, rings, dashes, squiggles and plus signs) burst out round it one after another, linger drifting and twinkling with a line boil, then shrink away and play again.",
3954
+ category: "comic",
3955
+ tags: ["cartoon", "comic", "doodle", "hand-drawn", "line-boil", "label", "burst", "announcement", "loop"],
3956
+ hold: 340,
3957
+ presets: [
3958
+ { name: "Night", values: { background: [0.08, 0.09, 0.13], card: [0.98, 0.84, 0.2], ink: [0.04, 0.04, 0.06], text: [0.08, 0.08, 0.1], doodleA: [0.98, 0.42, 0.62], doodleB: [0.4, 0.86, 1], doodleC: [1, 1, 1] } },
3959
+ { name: "New feature", values: { label: "NEW FEATURE", card: [0.39, 0.4, 0.95], text: [1, 1, 1], tilt: 5, doodleA: [0.98, 0.45, 0.2], doodleB: [0.13, 0.77, 0.55], doodleC: [0.39, 0.4, 0.95] } },
3960
+ { name: "Calm", values: { tilt: -2, wobble: 1, background: [0.96, 0.96, 0.94], doodleA: [0.55, 0.6, 0.7], doodleB: [0.55, 0.6, 0.7], doodleC: [0.55, 0.6, 0.7] } }
3961
+ ],
3962
+ derived: (v) => {
3963
+ const e = Math.max(0.6, textWidthEm(String(v.label)));
3964
+ const size = Math.min(30, 210 / e);
3965
+ const length = e * size;
3966
+ const [w, h] = [length + 44, size + 30];
3967
+ return { size: r15(size), length: r15(length), w: r15(w), h: r15(h), x: r15(DB_X - w / 2), y: r15(DB_Y - h / 2), base: r15(DB_Y + size * 0.36) };
3968
+ },
3969
+ motion: {
3970
+ svg: `<svg viewBox="0 0 400 240"><!-- {{label: text default='SOFTWARE UPDATE' label='Label'}} {{tilt: float min=-20 max=20 default=-6 step=0.5 label='Tilt (degrees)'}} {{background: color default=#FFF4DC label='Background'}} {{card: color default=#FFFFFF label='Card'}} {{text: color default=#111827 label='Text colour'}} {{ink: color default=#111827 label='Ink'}} {{doodleA: color default=#FF4D6D label='Doodle colour 1'}} {{doodleB: color default=#3B82F6 label='Doodle colour 2'}} {{doodleC: color default=#F5B700 label='Doodle colour 3'}} {{font: text default="${LETTERING}" label='Font'}} --><defs><filter id="doodleBurstBoil" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence id="dbNoise" type="turbulence" baseFrequency="0.04" numOctaves="2" seed="${DB_SEEDS[0]}" result="doodleBurstWarp"/><feDisplacementMap in="SourceGraphic" in2="doodleBurstWarp" scale="{{wobble: float min=0 max=8 default=3 step=0.5 label='Line boil'}}" xChannelSelector="R" yChannelSelector="G"/></filter></defs><rect width="400" height="240" fill="{{background}}"/><g filter="url(#doodleBurstBoil)" fill="none" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">${DB_DOODLES.map(
3971
+ (d, k) => `<g id="db${k}" transform="${dbPose(d, 0.55, 0, d.turn - 40)}" stroke="{{${d.colour}}}">${DB_SHAPES[d.shape]}</g>`
3972
+ ).join("")}</g><g id="dbCard" transform="${dbCard(0, -24)}" filter="url(#doodleBurstBoil)"><rect x="{{x}}" y="{{y}}" width="{{w}}" height="{{h}}" rx="9" fill="{{ink}}" transform="translate(6 6)"/><rect x="{{x}}" y="{{y}}" width="{{w}}" height="{{h}}" rx="9" fill="{{card}}" stroke="{{ink}}" stroke-width="3.5"/><text x="${DB_X}" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="900" fill="{{text}}">{{label}}</text></g></svg>`,
3973
+ animate: [
3974
+ // The card pops in past its size with a twist, breathes, then shrinks away.
3975
+ ...keys("dbCard", "transform", [
3976
+ [0, dbCard(0, -24)],
3977
+ [260, dbCard(1.16, 5)],
3978
+ [380, dbCard(0.95, -2)],
3979
+ [500, dbCard(1, 0)],
3980
+ [DB_OUT + 120, dbCard(1.04, 0)],
3981
+ [DB_OUT + 240, dbCard(1.12, -3)],
3982
+ [DB_END, dbCard(0, 16)]
3983
+ ], { ease: "out" }),
3984
+ // Each doodle pops out along its ray, drifts on twinkling, then pops off.
3985
+ ...DB_DOODLES.flatMap((d, k) => {
3986
+ const spin = k % 2 ? 14 : -14;
3987
+ const out = DB_OUT + k * 3 % 14 * 14;
3988
+ return keys(`db${k}`, "transform", [
3989
+ [0, dbPose(d, 0.55, 0, d.turn - 40)],
3990
+ [d.at, dbPose(d, 0.55, 0, d.turn - 40)],
3991
+ [d.at + 230, dbPose(d, 1.08, 1.3, d.turn + 6)],
3992
+ [d.at + 380, dbPose(d, 1, 1, d.turn)],
3993
+ [Math.round((d.at + 380 + out) / 2), dbPose(d, 1.03, k % 3 ? 0.82 : 1.12, d.turn + spin / 2)],
3994
+ [out, dbPose(d, 1.06, 1, d.turn + spin)],
3995
+ [out + 90, dbPose(d, 1.1, 1.2, d.turn + spin)],
3996
+ [out + 260, dbPose(d, 1.16, 0, d.turn + spin + 30)]
3997
+ ], { ease: "out" });
3998
+ }),
3999
+ // The line boil: the noise redrawn every few frames all the way round the loop.
4000
+ ...Array.from({ length: Math.ceil(DB_END / DB_BOIL) }, (_, k) => snap5("dbNoise", "seed", DB_SEEDS[k % DB_SEEDS.length], k * DB_BOIL))
4001
+ ]
4002
+ }
4003
+ };
4004
+ var comicEffects = [soundEffect, speechBubble, fightCloud, dizzyStars, animeEmotes, cartoonPoof, lineBoil, doodleBurst];
3571
4005
 
3572
4006
  // src/catalog/glow.ts
3573
4007
  var r16 = (v) => Math.round(v * 10) / 10;
@@ -3713,7 +4147,63 @@ var liftCard = {
3713
4147
  ]
3714
4148
  }
3715
4149
  };
3716
- var shadowEffects = [liftCard];
4150
+ var EL_X = 200;
4151
+ var EL_Y = 196;
4152
+ var EL_ISO = `translate(${EL_X} ${EL_Y}) matrix(0.866 0.5 -0.866 0.5 0 0)`;
4153
+ var EL_TIMES = { up1: 280, up2: 400, up3: 520, upMs: 680, down3: 2100, down2: 2220, down1: 2340, downMs: 620 };
4154
+ var elT = (name) => `{{el_${name}}}`;
4155
+ var elSlab = (body, side) => `<g transform="translate(0 ${side})"><g transform="${EL_ISO}">${body}</g></g><g transform="translate(0 ${side})"><g transform="${EL_ISO}" fill="{{shadow}}" opacity="0.2">${body}</g></g><g transform="${EL_ISO}">${body}</g>`;
4156
+ var EL_IMAGE = `<rect x="-66" y="-46" width="132" height="58" rx="7"/>`;
4157
+ var EL_BADGE = `<circle cx="62" cy="-40" r="17"/>`;
4158
+ var elShadow = (k, footprint) => `<g id="elS${k}" transform="translate(0 0)" opacity="0.34"><g transform="${EL_ISO}" fill="{{shadow}}" filter="url(#explodedLayersBlur${k})">${footprint}</g></g>`;
4159
+ var explodedLayers = {
4160
+ kind: "vector",
4161
+ id: "exploded-layers",
4162
+ name: "Exploded layers",
4163
+ description: "An isometric UI card whose layers (shadow plate, card base, image and text lines, floating badge) lift apart one after another, showing the gaps and the soft shadows between them, hold, then settle back together.",
4164
+ category: "shadow",
4165
+ tags: ["shadow", "isometric", "layers", "exploded", "card", "ui", "depth", "3d", "loop"],
4166
+ hold: 500,
4167
+ presets: [
4168
+ { name: "Dark", values: { background: [0.07, 0.08, 0.12], card: [0.16, 0.18, 0.25], accent: [0.35, 0.85, 0.95], shadow: [0, 0, 0] } },
4169
+ { name: "Coral", values: { background: [1, 0.95, 0.92], accent: [0.97, 0.42, 0.36], shadow: [0.4, 0.12, 0.08], lift: 38 } },
4170
+ { name: "Slow and deep", values: { lift: 44, speed: 0.6 } }
4171
+ ],
4172
+ derived: (v) => {
4173
+ const speed = Math.max(0.1, Number(v.speed));
4174
+ const lift = Number(v.lift);
4175
+ return {
4176
+ ...Object.fromEntries(Object.entries(EL_TIMES).map(([name, ms]) => [`el_${name}`, Math.round(ms / speed)])),
4177
+ up: r17(-lift),
4178
+ fall: r17(lift * 0.92),
4179
+ spread: r17(lift * 0.12)
4180
+ };
4181
+ },
4182
+ motion: {
4183
+ svg: `<svg viewBox="0 0 400 300"><!-- {{background: color default=#ECEFF7 label='Background'}} {{card: color default=#FFFFFF label='Card'}} {{accent: color default=#6366F1 label='Accent'}} {{shadow: color default=#1E1B4B label='Shadow colour'}} {{lift: float min=10 max=46 default=34 step=1 label='Lift distance'}} {{speed: float min=0.25 max=3 default=1 step=0.05 label='Speed'}} --><defs>${[0, 1, 2, 3].map((k) => `<filter id="explodedLayersBlur${k}" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur id="elBlur${k}" stdDeviation="${k ? 1.5 : 4}"/></filter>`).join("")}</defs><rect width="400" height="300" fill="{{background}}"/><g id="elS0" transform="translate(0 6)" opacity="0.32"><g transform="${EL_ISO}" fill="{{shadow}}" filter="url(#explodedLayersBlur0)"><rect x="-84" y="-62" width="168" height="124" rx="14"/></g></g><g id="elL1" transform="translate(0 0)"><g fill="{{card}}">${elSlab(`<rect x="-80" y="-58" width="160" height="116" rx="12"/>`, 7)}</g><g transform="${EL_ISO}"><rect x="-80" y="-58" width="160" height="116" rx="12" fill="none" stroke="#FFFFFF" stroke-opacity="0.4" stroke-width="1"/></g>${elShadow(1, `${EL_IMAGE}<rect x="-66" y="22" width="112" height="9" rx="4.5"/><rect x="-66" y="38" width="76" height="9" rx="4.5"/>`)}<g id="elL2" transform="translate(0 0)"><g fill="{{accent}}">${elSlab(EL_IMAGE, 3)}</g><g transform="${EL_ISO}"><path d="M-60 6 L-30 -22 L-8 0 L10 -14 L60 6 Z" fill="#FFFFFF" opacity="0.4"/><circle cx="34" cy="-28" r="8" fill="#FFFFFF" opacity="0.55"/><rect x="-66" y="-46" width="132" height="58" rx="7" fill="none" stroke="#FFFFFF" stroke-opacity="0.5" stroke-width="1"/><g fill="{{accent}}" opacity="0.35"><rect x="-66" y="22" width="112" height="9" rx="4.5"/><rect x="-66" y="38" width="76" height="9" rx="4.5"/></g></g>${elShadow(2, EL_BADGE)}<g id="elL3" transform="translate(0 0)"><g fill="{{accent}}">${elSlab(EL_BADGE, 4)}</g><g transform="${EL_ISO}"><circle cx="62" cy="-40" r="12.5" fill="none" stroke="#FFFFFF" stroke-opacity="0.45" stroke-width="1.5"/><path d="M55 -40 L60 -35 L69 -45" fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></g></g></g></g></svg>`,
4184
+ animate: [
4185
+ // Up: each layer in turn rises off the one below, its shadow falling back and spreading.
4186
+ ...[1, 2, 3].flatMap((k) => [
4187
+ { id: `elL${k}`, attrs: { transform: ["translate(0 0)", "translate(0 {{up}})"] }, delay: elT(`up${k}`), ms: elT("upMs"), ease: "in-out" },
4188
+ { id: `elL${k}`, attrs: { transform: ["translate(0 {{up}})", "translate(0 0)"] }, delay: elT(`down${k}`), ms: elT("downMs"), ease: "in-out" }
4189
+ ]),
4190
+ ...[1, 2, 3].flatMap((k) => {
4191
+ const s = `elS${k - 1}`;
4192
+ const rest = k === 1 ? "translate(0 6)" : "translate(0 0)";
4193
+ const open = k === 1 ? "translate({{spread}} {{fall}})" : "translate({{spread}} {{spread}})";
4194
+ const [o0, o1] = k === 1 ? [0.32, 0.18] : [0.34, 0.2];
4195
+ const [b0, b1] = k === 1 ? [4, 11] : [1.5, 6];
4196
+ return [
4197
+ { id: s, attrs: { transform: [rest, open], opacity: [o0, o1] }, delay: elT(`up${k}`), ms: elT("upMs"), ease: "in-out" },
4198
+ { id: s, attrs: { transform: [open, rest], opacity: [o1, o0] }, delay: elT(`down${k}`), ms: elT("downMs"), ease: "in-out" },
4199
+ { id: `elBlur${k - 1}`, attrs: { stdDeviation: [b0, b1] }, delay: elT(`up${k}`), ms: elT("upMs"), ease: "in-out" },
4200
+ { id: `elBlur${k - 1}`, attrs: { stdDeviation: [b1, b0] }, delay: elT(`down${k}`), ms: elT("downMs"), ease: "in-out" }
4201
+ ];
4202
+ })
4203
+ ]
4204
+ }
4205
+ };
4206
+ var shadowEffects = [liftCard, explodedLayers];
3717
4207
 
3718
4208
  // src/catalog/lighting.ts
3719
4209
  var sway = (deg) => `rotate(${deg} 120 0)`;
@@ -7031,7 +7521,399 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
7031
7521
  { name: "No surface", values: { uSurface: false, uLevel: 0.15, uTint: [1, 1, 1] } }
7032
7522
  ]
7033
7523
  };
7034
- var splatterShaders = [paintSplatter, inkSplash, splashCrown, gooSplat, paintRoller, waterSplash];
7524
+ var SMAX = `
7525
+ float smax(float a, float b, float k) {
7526
+ return -smin(-a, -b, k);
7527
+ }`.trim();
7528
+ var GOO_SHADE = `
7529
+ vec3 gooShade(vec3 goo, vec3 n, float thick, float gloss) {
7530
+ vec3 light = normalize(vec3(-0.45, 0.6, 1.0));
7531
+ vec3 col = mix(goo * 1.15, goo * 0.62, thick * 0.6) * (0.78 + 0.22 * dot(n, light));
7532
+ col = mix(col, vec3(1.0), pow(1.0 - n.z, 2.0) * 0.35);
7533
+ col *= 1.0 - 0.25 * smoothstep(0.0, 0.6, -n.y) * (1.0 - thick);
7534
+ float rz = max(reflect(-light, n).z, 0.0);
7535
+ return min(col + gloss * (pow(rz, 40.0) * 0.9 + pow(rz, 6.0) * 0.12), vec3(1.0));
7536
+ }`.trim();
7537
+ var slimeCurtainWipe = {
7538
+ kind: "shader",
7539
+ id: "slime-curtain-wipe",
7540
+ name: "Slime curtain wipe",
7541
+ description: "Thick, glossy slime pouring down from the top with a wobbling front and long beaded drips until it covers the frame, then sliding off in stretchy strings to reveal the next picture.",
7542
+ category: "splatter",
7543
+ video: "transition",
7544
+ tags: ["splatter", "slime", "goo", "transition", "image", "reveal", "slideshow", "playful"],
7545
+ loop: 6,
7546
+ source: glsl(
7547
+ `// Slime curtain wipe: goo pours down over the From picture, then slides off to show the To one, and back again.
7548
+ uniform sampler2D uFrom; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.landscape} label="From image"
7549
+ uniform sampler2D uTo; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.city} label="To image"
7550
+ uniform bool uSwap; // @control default=true label="Reveal the To image"
7551
+ uniform vec3 uColorA; // @control color default=#FF4FA3 label="Goo"
7552
+ uniform vec3 uColorB; // @control color default=#A3E635 label="Swirl colour"
7553
+ uniform float uMarble; // @control min=0 max=1 default=0.3 label="Swirl amount"
7554
+ uniform int uDripCount; // @control min=0 max=24 default=14 label="Drips"
7555
+ uniform float uThickness; // @control min=0.4 max=2 default=1 label="Thickness"
7556
+ uniform float uGloss; // @control min=0 max=1 default=0.9 label="Gloss"
7557
+ uniform bool uAnimate; // @control default=true label="Animate"
7558
+ uniform float uProgress; // @control min=0 max=1 default=0.45 label="Progress (when still)"
7559
+ ${BACKDROP_CONTROLS}`,
7560
+ HASH,
7561
+ NOISE,
7562
+ FBM,
7563
+ SMIN,
7564
+ SMAX,
7565
+ SEGMENT,
7566
+ GOO_SHADE,
7567
+ PICTURE,
7568
+ `// The pass: x cover (0\u20131), y reveal (0\u20131), z seconds into it, w a seed.
7569
+ vec4 gPass;
7570
+
7571
+ // The pouring front's height at x: uneven, sagging in fat bellies, wobbling as it comes.
7572
+ float frontAt(float x) {
7573
+ float s = gPass.w;
7574
+ float lag = 0.14 * (vnoise(vec2(x * 2.3 + s, 0.5)) - 0.5) + 0.05 * (vnoise(vec2(x * 6.1 + s, 2.5)) - 0.5);
7575
+ float belly = 0.022 * sqrt(abs(sin(x * 6.5 + s)) + 0.02);
7576
+ float wob = 0.012 * sin(x * 9.0 + gPass.z * 5.0 + s) * (1.0 - gPass.y);
7577
+ return mix(1.13, -0.3, gPass.x) - 0.75 * gPass.y + lag - belly + wob;
7578
+ }
7579
+
7580
+ // The top edge as the goo slides off.
7581
+ float topAt(float x) {
7582
+ float e = gPass.y * gPass.y * (3.0 - 2.0 * gPass.y);
7583
+ return mix(1.1, -0.16, e) + 0.04 * (vnoise(vec2(x * 3.1 + gPass.w, 7.0)) - 0.5) - 0.025 * sin(x * 2.0 + gPass.w);
7584
+ }
7585
+
7586
+ // The goo's signed distance (frame heights) at P: the curtain, its drips and, sliding off, its strings.
7587
+ float curtain(vec2 P, float aspect) {
7588
+ float d = frontAt(P.x) - P.y;
7589
+ float s = gPass.w;
7590
+ for (int k = 0; k < 24; k++) {
7591
+ if (k >= uDripCount) break;
7592
+ vec3 h = vec3(hash22(vec2(float(k) + 0.5, s + 3.3)), hash12(vec2(s + 1.7, float(k) * 2.9)));
7593
+ float x = (float(k) + 0.5 + (h.x - 0.5) * 0.7) / float(uDripCount) * aspect;
7594
+ if (abs(P.x - x) > 0.12) continue;
7595
+ float fx = frontAt(x);
7596
+ float len = (0.08 + 0.3 * h.y) * smoothstep(0.08, 0.7, gPass.x) * (1.0 + 0.8 * gPass.y) * (1.0 + 0.05 * sin(gPass.z * 4.0 + h.x * 6.28));
7597
+ float w = (0.01 + 0.015 * h.z) * uThickness;
7598
+ vec2 b = vec2(x, fx - len);
7599
+ vec2 sg = segment(P, vec2(x, fx + 0.03), b);
7600
+ d = smin(d, sg.x - w * mix(1.15, 0.7, sg.y), 0.03);
7601
+ d = smin(d, length((P - b + vec2(0.0, w * 0.5)) * vec2(1.0, 0.85)) - w * 1.6, 0.012);
7602
+ }
7603
+ if (gPass.y > 0.0) {
7604
+ d = smax(d, P.y - topAt(P.x), 0.02);
7605
+ // Strings still stuck to the top, stretching thin and snapping.
7606
+ for (int j = 0; j < 6; j++) {
7607
+ vec3 h = vec3(hash22(vec2(float(j) + 0.5, s + 8.1)), hash12(vec2(s + 4.3, float(j) * 1.3)));
7608
+ float x = (float(j) + 0.2 + 0.6 * h.x) / 6.0 * aspect;
7609
+ float wj = (0.016 + 0.02 * h.y) * uThickness * (1.0 - smoothstep(0.3, 0.7 + 0.25 * h.z, gPass.y));
7610
+ if (wj <= 0.0 || abs(P.x - x) > 0.06) continue;
7611
+ vec2 sg = segment(P, vec2(x, topAt(x) - 0.01), vec2(x, 1.1));
7612
+ d = smin(d, sg.x - wj * mix(1.0, 0.55, sin(3.14159 * sg.y)), 0.05);
7613
+ }
7614
+ }
7615
+ return d;
7616
+ }
7617
+
7618
+ // The goo's surface height: a dome falling away at the rim, rippled where it runs.
7619
+ float gooHeight(vec2 P, float aspect, out float d) {
7620
+ d = curtain(P, aspect);
7621
+ float k = 0.028 * uThickness;
7622
+ float ripple = fbm(vec2(P.x * 9.0, (P.y + gPass.z * 0.35) * 2.5) + gPass.w, 3);
7623
+ return -1.6 * k * exp(min(d, 0.0) / k) + 0.03 * uThickness * ripple * smoothstep(-0.01, -0.09, d);
7624
+ }
7625
+
7626
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
7627
+ vec2 uv = fragCoord / iResolution.xy;
7628
+ float px = 1.0 / iResolution.y;
7629
+ float aspect = iResolution.x / iResolution.y;
7630
+ vec2 P = vec2(uv.x * aspect, uv.y);
7631
+ // Two passes a loop: pour over the From picture and slide off the To one, then back.
7632
+ float t = mod(iTime, 6.0);
7633
+ bool second = uAnimate && t >= 3.0;
7634
+ float prog = uAnimate ? (t - (second ? 3.0 : 0.0)) / 3.0 : uProgress;
7635
+ gPass = vec4(smoothstep(0.03, 0.42, prog), smoothstep(0.5, 0.98, prog), prog * 3.0, second ? 7.0 : 0.0);
7636
+ vec4 a = pictureRGBA(uFrom, uv);
7637
+ vec4 b = uSwap ? pictureRGBA(uTo, uv) : a;
7638
+ vec4 below = gPass.y > 0.0 ? (second ? a : b) : (second ? b : a);
7639
+ // A soft shadow the goo casts down on the picture.
7640
+ float dS = curtain(P + vec2(-0.006, 0.016), aspect);
7641
+ below.rgb *= 1.0 - 0.38 * smoothstep(0.022, -0.012, dS);
7642
+ float d;
7643
+ float h = gooHeight(P, aspect, d);
7644
+ float cover = clamp(0.5 - d / px, 0.0, 1.0);
7645
+ vec4 col = below;
7646
+ if (cover > 0.0) {
7647
+ float eps = px;
7648
+ float dd;
7649
+ vec2 g = vec2(gooHeight(P + vec2(eps, 0.0), aspect, dd), gooHeight(P + vec2(0.0, eps), aspect, dd)) - h;
7650
+ vec3 n = normalize(vec3(-g / eps, 1.0));
7651
+ // Swirls of the second colour running through it.
7652
+ float v = fbm(vec2(P.x * 2.5 + gPass.w, (P.y + gPass.z * 0.35) * 0.9), 4);
7653
+ float m = uMarble > 0.0 ? smoothstep(0.74 - 0.4 * uMarble, 0.75 - 0.4 * uMarble, v) : 0.0;
7654
+ vec3 goo = mix(uColorA, uColorB, m);
7655
+ col = mix(below, vec4(gooShade(goo, n, smoothstep(0.0, -0.08, d), uGloss), 1.0), cover);
7656
+ }
7657
+ fragColor = col;
7658
+ }`
7659
+ ),
7660
+ presets: [
7661
+ { name: "Lime slime", values: { uColorA: [0.64, 0.9, 0.21], uColorB: [1, 0.31, 0.64], uMarble: 0.15 } },
7662
+ { name: "Clear off", values: { uSwap: false, uMarble: 0.55, uDripCount: 20 } },
7663
+ { name: "Chocolate", values: { uColorA: [0.36, 0.2, 0.11], uColorB: [0.55, 0.33, 0.18], uMarble: 0.4, uThickness: 1.5, uGloss: 0.75 } }
7664
+ ]
7665
+ };
7666
+ var gooeyFrame = {
7667
+ kind: "shader",
7668
+ id: "gooey-frame",
7669
+ name: "Gooey frame",
7670
+ description: "A wobbling rim of glossy slime round a rounded frame, with drips swelling on its edges, sliding down and dropping off in beads, for cards and buttons.",
7671
+ category: "splatter",
7672
+ tags: ["splatter", "slime", "goo", "border", "frame", "ui", "card", "overlay", "playful"],
7673
+ transparent: true,
7674
+ loop: 8,
7675
+ source: glsl(
7676
+ `// Gooey frame: a slime rim round a rounded rectangle, wobbling, with drips that swell, run and drop.
7677
+ uniform vec3 uColor; // @control color default=#A3E635 label="Goo"
7678
+ uniform float uInset; // @control min=0 max=0.3 default=0.14 label="Inset"
7679
+ uniform float uRadius; // @control min=0 max=0.5 default=0.08 label="Corner radius"
7680
+ uniform float uThickness; // @control min=0.005 max=0.08 default=0.022 label="Thickness"
7681
+ uniform float uDrips; // @control min=0 max=1 default=0.6 label="Drip rate"
7682
+ uniform float uWobble; // @control min=0 max=1 default=0.5 label="Wobble"
7683
+ uniform float uGloss; // @control min=0 max=1 default=0.9 label="Gloss"
7684
+ uniform bool uShadow; // @control default=true label="Shadow"`,
7685
+ HASH,
7686
+ NOISE,
7687
+ SMIN,
7688
+ SEGMENT,
7689
+ OVER2,
7690
+ GOO_SHADE,
7691
+ `float boxSdf(vec2 p, vec2 b, float r) {
7692
+ vec2 q = abs(p) - b + r;
7693
+ return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
7694
+ }
7695
+
7696
+ // The rim's signed distance (frame heights) at p, T seconds into the loop.
7697
+ float gooFrame(vec2 p, vec2 box, float rad, float T) {
7698
+ // Noise drifting round a circle, so the wobble loops.
7699
+ float ph = 6.28318 * T / 8.0;
7700
+ float n1 = vnoise(p * 6.0 + vec2(cos(ph), sin(ph)) * 1.3);
7701
+ float n2 = vnoise(p * 14.0 + vec2(cos(2.0 * ph + 1.0), sin(2.0 * ph + 1.0)) * 0.9 + 9.0);
7702
+ float th = uThickness * (1.0 + uWobble * ((n1 - 0.5) * 1.1 + (n2 - 0.5) * 0.4));
7703
+ // It sags, so it is fatter along the bottom.
7704
+ th *= mix(1.0, 1.35, smoothstep(0.2, -1.0, p.y / box.y));
7705
+ float d = abs(boxSdf(p, box, rad) + uWobble * uThickness * (n2 - 0.5) * 0.8) - th;
7706
+ // Drips: each swells on an edge, runs down and, off the bottom, drops away; on the top it soaks back in.
7707
+ for (int k = 0; k < 12; k++) {
7708
+ vec3 h = vec3(hash22(vec2(float(k) + 0.5, 7.3)), hash12(vec2(4.1, float(k) * 1.9 + 0.3)));
7709
+ if (h.z > uDrips) continue;
7710
+ float cyc = mod(T + h.x * 8.0, 8.0);
7711
+ float which = floor(cyc / 4.0);
7712
+ float a = cyc - which * 4.0;
7713
+ vec2 hh = hash22(vec2(float(k) * 3.1 + which * 17.0, 2.2));
7714
+ bool top = hh.y < 0.3;
7715
+ float x = (hh.x * 2.0 - 1.0) * max(box.x - rad - uThickness * 2.0, 0.0);
7716
+ if (abs(p.x - x) > 0.12) continue;
7717
+ float y0 = top ? box.y : -box.y;
7718
+ float grow = smoothstep(0.0, 0.8, a);
7719
+ float w = uThickness * (0.6 + 0.35 * h.y) * (top ? 1.0 - smoothstep(2.8, 3.9, a) : 1.0);
7720
+ float lmax = top ? 0.05 + 0.1 * h.y : 0.07 + 0.15 * h.y;
7721
+ float run = smoothstep(0.6, 2.6, a);
7722
+ bool dropped = !top && a > 2.6;
7723
+ float len = lmax * run * (dropped ? 1.0 - smoothstep(2.6, 3.3, a) : 1.0);
7724
+ vec2 sg = segment(p, vec2(x, y0), vec2(x, y0 - len));
7725
+ d = smin(d, sg.x - w * mix(0.8, 0.5, sg.y) * grow, w * 1.2 * grow + 1e-4);
7726
+ float fall = dropped ? 1.25 * (a - 2.6) * (a - 2.6) : 0.0;
7727
+ vec2 bead = vec2(x, y0 - lmax * run - fall - w * 0.3);
7728
+ float db = length((p - bead) * vec2(1.0, 0.85)) - w * mix(0.8, 1.5, grow);
7729
+ d = dropped ? min(d, db) : smin(d, db, w * 0.8 * grow + 1e-4);
7730
+ }
7731
+ return d;
7732
+ }
7733
+
7734
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
7735
+ float px = 1.0 / iResolution.y;
7736
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
7737
+ vec2 box = max(0.5 * iResolution.xy * px - uInset, vec2(0.05));
7738
+ float rad = min(uRadius, min(box.x, box.y));
7739
+ float T = mod(iTime, 8.0);
7740
+ vec4 acc = vec4(0.0);
7741
+ float d = gooFrame(p, box, rad, T);
7742
+ float cover = clamp(0.5 - d / px, 0.0, 1.0);
7743
+ if (uShadow) {
7744
+ float dS = gooFrame(p + vec2(-0.007, 0.013), box, rad, T);
7745
+ acc = over(acc, vec3(0.0), 0.32 * smoothstep(0.02, -0.01, dS) * (1.0 - cover));
7746
+ }
7747
+ if (cover > 0.0) {
7748
+ float eps = px;
7749
+ vec2 g = vec2(gooFrame(p + vec2(eps, 0.0), box, rad, T), gooFrame(p + vec2(0.0, eps), box, rad, T)) - d;
7750
+ g /= eps;
7751
+ vec3 n = normalize(vec3(g * exp(min(d, 0.0) / (uThickness * 0.9)) * 1.6, 1.0));
7752
+ float thick = smoothstep(0.0, -uThickness * 1.2, d);
7753
+ acc = over(acc, gooShade(uColor, n, thick, uGloss), cover * mix(0.97, 0.9, thick));
7754
+ }
7755
+ fragColor = unpremultiply(acc);
7756
+ }`
7757
+ ),
7758
+ presets: [
7759
+ { name: "Slime button", values: { uInset: 0.05, uRadius: 0.5, uThickness: 0.016, uDrips: 0.4 } },
7760
+ { name: "Bubblegum", values: { uColor: [1, 0.42, 0.7], uRadius: 0.12, uWobble: 0.7 } },
7761
+ { name: "Heavy drip", values: { uThickness: 0.035, uDrips: 1, uWobble: 0.8, uInset: 0.18 } }
7762
+ ]
7763
+ };
7764
+ var gooVortex = {
7765
+ kind: "shader",
7766
+ id: "goo-vortex",
7767
+ name: "Goo vortex",
7768
+ description: "Two glossy ribbons of goo spiralling in round the centre, coiling into a ball and flinging out to splat round the frame, an intro on a loop.",
7769
+ category: "splatter",
7770
+ tags: ["splatter", "slime", "goo", "intro", "spiral", "paint", "playful"],
7771
+ loop: 6,
7772
+ source: glsl(
7773
+ `// Goo vortex: ribbons of goo spiral in, coil up, fling out and splat; then it starts again.
7774
+ uniform vec3 uColorA; // @control color default=#FF4FA3 label="Goo 1"
7775
+ uniform vec3 uColorB; // @control color default=#A3E635 label="Goo 2"
7776
+ uniform vec3 uBackground; // @control color default=#FFD93D label="Background"
7777
+ uniform float uSpin; // @control min=0 max=3 default=1 label="Spin speed"
7778
+ uniform float uWidth; // @control min=0.015 max=0.12 default=0.05 label="Ribbon width"
7779
+ uniform int uArms; // @control min=1 max=6 default=2 label="Arms"
7780
+ uniform float uSplat; // @control min=0 max=1 default=0.7 label="Splat amount"
7781
+ uniform float uGloss; // @control min=0 max=1 default=0.9 label="Gloss"`,
7782
+ HASH,
7783
+ NOISE,
7784
+ SMIN,
7785
+ SMAX,
7786
+ SEGMENT,
7787
+ GOO_SHADE,
7788
+ `// The union of two (distance, colour) pairs, melted together within k.
7789
+ vec2 meld(vec2 a, vec2 b, float k) {
7790
+ float h = clamp(0.5 + 0.5 * (b.x - a.x) / k, 0.0, 1.0);
7791
+ return vec2(mix(b.x, a.x, h) - k * h * (1.0 - h), mix(b.y, a.y, smoothstep(0.3, 0.7, h)));
7792
+ }
7793
+
7794
+ // A splat in units of its radius, age seconds after it landed flying along dir.
7795
+ float splatSd(vec2 q, float seed, float age, vec2 dir) {
7796
+ float grow = 1.0 - exp(-age * 14.0);
7797
+ float wob = 0.25 * exp(-age * 5.0) * cos(age * 24.0);
7798
+ float s = mix(0.45, 1.0, grow);
7799
+ // Its own frame: x along the flight. It lands squashed and wobbles back.
7800
+ vec2 L = vec2(dot(q, dir), dot(q, vec2(-dir.y, dir.x)));
7801
+ vec2 b = L / vec2(1.0 - wob, 1.0 + wob) / s;
7802
+ float d = length(b) - 0.5;
7803
+ for (int k = 0; k < 5; k++) {
7804
+ vec2 h = hash22(vec2(seed, float(k) + 0.5));
7805
+ float a = h.x * 6.28318;
7806
+ d = smin(d, length(b - vec2(cos(a), sin(a)) * (0.3 + 0.3 * h.y)) - 0.16 - 0.12 * hash12(h + seed), 0.2);
7807
+ }
7808
+ d = (d + (vnoise(b * 3.5 + seed) - 0.5) * 0.08) * s;
7809
+ // Rays thrown on ahead, each ending in a drop.
7810
+ for (int k = 0; k < 4; k++) {
7811
+ vec3 h = vec3(hash22(vec2(seed + 3.3, float(k) + 0.5)), hash12(vec2(float(k) * 2.1, seed)));
7812
+ float a = (h.x - 0.5) * 2.2;
7813
+ vec2 rd = vec2(cos(a), sin(a));
7814
+ float len = (0.5 + 0.55 * h.y) * grow;
7815
+ vec2 sg = segment(L, rd * 0.3, rd * len);
7816
+ d = smin(d, sg.x - mix(0.17, 0.07, sg.y), 0.14);
7817
+ d = smin(d, length(L - rd * len) - 0.1 - 0.05 * h.z, 0.08);
7818
+ d = min(d, length(L - rd * (len + (0.2 + 0.2 * h.z) * grow)) - (0.05 + 0.06 * h.z) * grow);
7819
+ }
7820
+ // Two drips running down, with beads on their ends.
7821
+ for (int j = 0; j < 2; j++) {
7822
+ vec2 h = hash22(vec2(seed + 6.1, float(j) + 0.5));
7823
+ vec2 top = vec2((h.x - 0.5) * 0.8, -0.3);
7824
+ float len = (0.3 + 0.8 * h.y) * (1.0 - exp(-max(age - 0.3, 0.0) * 0.6));
7825
+ float w = 0.08;
7826
+ vec2 sg = segment(q, top, top - vec2(0.0, len));
7827
+ d = smin(d, sg.x - w * mix(1.0, 0.7, sg.y), 0.1);
7828
+ d = smin(d, length(q - top + vec2(0.0, len + w * 0.4)) - w * 1.4, 0.05);
7829
+ }
7830
+ return d;
7831
+ }
7832
+
7833
+ // The goo at p, T seconds into the loop: x its signed distance (frame heights), y its colour (0 the first, 1 the second).
7834
+ vec2 vortex(vec2 p, float T, float aspect) {
7835
+ float N = float(uArms);
7836
+ float r = max(length(p), 1e-4);
7837
+ float lr = log(r);
7838
+ float ang = atan(p.y, p.x);
7839
+ // The ribbons are arms of a log spiral turning in; as they fling out it unwinds.
7840
+ float fl = smoothstep(2.45, 3.3, T);
7841
+ float c = mix(2.4, 0.4, fl);
7842
+ float f = (ang - uSpin * T * 2.2 - c * lr) * N / 6.28318;
7843
+ float idx = floor(f + 0.5);
7844
+ float del = (f - idx) * 6.28318 / N;
7845
+ float perp = r * del / sqrt(1.0 + c * c);
7846
+ float arm = mod(idx, N);
7847
+ float other = mod(mod(idx + (del > 0.0 ? 1.0 : -1.0), N), 2.0);
7848
+ float colour = mix(mod(arm, 2.0), other, 0.5 * smoothstep(3.14159 / N - 0.05, 3.14159 / N, abs(del)));
7849
+ // Thinner at the middle, twisting along its length, and breaking into blobs as it flings.
7850
+ float w = uWidth * (0.35 + 0.65 * smoothstep(0.0, 0.3, r)) * (0.65 + 0.35 * cos(lr * 10.0 - T * 6.0 + arm * 3.4));
7851
+ float brk = vnoise(vec2(lr * 7.0 + arm * 3.1, arm * 1.3 + 2.0));
7852
+ w *= smoothstep(fl * 1.1 - 0.1, fl * 1.1 + 0.1, brk);
7853
+ float e = clamp(T / 2.4, 0.0, 1.0);
7854
+ float head = mix(1.05, 0.03, 1.0 - (1.0 - e) * (1.0 - e)) + fl * fl * 1.3;
7855
+ vec2 res = vec2(smax(abs(perp) - w, head - r, uWidth * 0.8), colour);
7856
+ // The coil balling up in the middle before it bursts.
7857
+ float ballR = 0.12 * smoothstep(1.6, 2.5, T) * (1.0 - smoothstep(2.65, 2.95, T));
7858
+ if (ballR > 0.0) res = meld(res, vec2(r - ballR * (1.0 + 0.06 * sin(ang * 3.0 + T * 9.0)), colour), 0.04);
7859
+ // Blobs flung out from the burst, splatting round the frame.
7860
+ int count = int(uSplat * 12.0 + 0.5);
7861
+ for (int k = 0; k < 12; k++) {
7862
+ if (k >= count) break;
7863
+ vec3 h = vec3(hash22(vec2(float(k) + 0.5, 3.1)), hash12(vec2(5.7, float(k) * 1.3)));
7864
+ float a0 = float(k) * 2.39996 + h.x * 0.8 + 0.6;
7865
+ vec2 rest = vec2(cos(a0) * 0.5 * aspect, sin(a0) * 0.5) * (0.35 + 0.55 * h.y);
7866
+ vec2 dir = normalize(rest);
7867
+ float R = (0.065 + 0.06 * h.z) * (0.7 + 0.5 * uSplat);
7868
+ float age = T - 2.75 - 0.12 * h.y;
7869
+ if (age < 0.0) continue;
7870
+ float ff = min(age / 0.3, 1.0);
7871
+ vec2 pos = rest * (1.0 - (1.0 - ff) * (1.0 - ff));
7872
+ float ds;
7873
+ if (age < 0.3) {
7874
+ vec2 sg = segment(p, pos - dir * R * 2.5 * (1.0 - ff), pos);
7875
+ ds = sg.x - R * 0.6 * (0.7 + 0.3 * sg.y);
7876
+ } else {
7877
+ vec2 q = (p - pos) / R;
7878
+ float lq = length(q);
7879
+ ds = (lq > 3.4 ? lq - 2.8 : splatSd(q, float(k) * 7.3 + 1.0, age - 0.3, dir)) * R;
7880
+ }
7881
+ res = meld(res, vec2(ds, mod(float(k), 2.0)), 0.015);
7882
+ }
7883
+ return res;
7884
+ }
7885
+
7886
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
7887
+ float px = 1.0 / iResolution.y;
7888
+ float aspect = iResolution.x / iResolution.y;
7889
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
7890
+ float T = mod(iTime, 6.0);
7891
+ float fade = 1.0 - smoothstep(5.2, 5.95, T);
7892
+ vec3 bg = uBackground * (1.06 - 0.22 * dot(p, p));
7893
+ // A soft shadow under the goo.
7894
+ float dS = vortex(p + vec2(-0.012, 0.022), T, aspect).x;
7895
+ bg *= 1.0 - 0.3 * smoothstep(0.03, -0.02, dS) * fade;
7896
+ vec2 v = vortex(p, T, aspect);
7897
+ float cover = clamp(0.5 - v.x / px, 0.0, 1.0) * fade;
7898
+ vec3 col = bg;
7899
+ if (cover > 0.0) {
7900
+ float eps = px;
7901
+ vec2 g = vec2(vortex(p + vec2(eps, 0.0), T, aspect).x, vortex(p + vec2(0.0, eps), T, aspect).x) - v.x;
7902
+ g /= eps;
7903
+ vec3 n = normalize(vec3(g * exp(min(v.x, 0.0) / (uWidth * 1.1)) * 1.6, 1.0));
7904
+ float thick = smoothstep(0.0, -uWidth * 1.2, v.x);
7905
+ col = mix(bg, gooShade(mix(uColorA, uColorB, v.y), n, thick, uGloss), cover);
7906
+ }
7907
+ fragColor = vec4(col, 1.0);
7908
+ }`
7909
+ ),
7910
+ presets: [
7911
+ { name: "Three-way", values: { uArms: 3, uWidth: 0.04, uSpin: 1.4 } },
7912
+ { name: "Candy", values: { uColorA: [0.5, 0.75, 1], uColorB: [1, 0.62, 0.85], uBackground: [0.98, 0.95, 0.9] } },
7913
+ { name: "Calm", values: { uSpin: 0.4, uSplat: 0.3, uWidth: 0.07 } }
7914
+ ]
7915
+ };
7916
+ var splatterShaders = [paintSplatter, inkSplash, splashCrown, gooSplat, paintRoller, waterSplash, slimeCurtainWipe, gooeyFrame, gooVortex];
7035
7917
 
7036
7918
  // src/catalog/shaders/bubble.ts
7037
7919
  var OVER3 = `
@@ -12108,6 +12990,82 @@ ${BACKDROP_CONTROLS}`,
12108
12990
  { name: "Crisp", values: { uEdge: 0.015, uScale: 5 } }
12109
12991
  ]
12110
12992
  };
12993
+ var transitionWhipPan = {
12994
+ kind: "shader",
12995
+ id: "transition-whip-pan",
12996
+ name: "Whip pan",
12997
+ description: "A beat-cut whip pan or zoom punch: the first image flies off in a motion blur that peaks at the cut, and the next one lands from the other side.",
12998
+ category: "transition",
12999
+ video: "transition",
13000
+ tags: ["transition", "whip", "pan", "zoom", "motion blur", "image", "cut"],
13001
+ loop: 4,
13002
+ source: glsl(
13003
+ `// Whip pan: From accelerates away under a motion blur (directional, or radial in zoom mode), swaps at mid-progress, and To decelerates in.
13004
+ ${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
13005
+ uniform int uMode; // @control min=0 max=1 default=0 label="Mode (0 whip, 1 zoom)"
13006
+ uniform float uAngle; // @control min=0 max=360 default=0 label="Direction (\xB0)"
13007
+ uniform float uStrength; // @control min=0 max=1 default=0.6 label="Blur strength"
13008
+ uniform float uPunch; // @control min=0 max=1 default=0.15 label="Scale punch"
13009
+ ${BACKDROP_CONTROLS}`,
13010
+ PICTURE,
13011
+ COMMON,
13012
+ `// Mirrored at the frame edges, so a moving image never runs out of picture.
13013
+ vec4 folded(sampler2D tex, vec2 uv) {
13014
+ return coverRGBA(tex, 1.0 - abs(1.0 - mod(uv, 2.0)));
13015
+ }
13016
+
13017
+ // 24 taps spread len along dir, centred on uv: a directional motion blur.
13018
+ vec4 smear(sampler2D tex, vec2 uv, vec2 dir, float len) {
13019
+ vec4 sum = vec4(0.0);
13020
+ for (int i = 0; i < 24; i++) {
13021
+ sum += folded(tex, uv + dir * len * ((float(i) + 0.5) / 24.0 - 0.5));
13022
+ }
13023
+ return sum / 24.0;
13024
+ }
13025
+
13026
+ // 24 taps from uv towards the centre, a zoom blur.
13027
+ vec4 zoomSmear(sampler2D tex, vec2 uv, float len) {
13028
+ vec4 sum = vec4(0.0);
13029
+ for (int i = 0; i < 24; i++) {
13030
+ float k = 1.0 - len * (float(i) + 0.5) / 24.0;
13031
+ sum += folded(tex, 0.5 + (uv - 0.5) * k);
13032
+ }
13033
+ return sum / 24.0;
13034
+ }
13035
+
13036
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13037
+ vec2 uv = fragCoord / iResolution.xy;
13038
+ float t = progress();
13039
+ bool second = t >= 0.5;
13040
+ // Each half runs 0 at its far end to 1 at the cut; travel eases in (u\xB3), so speed (u\xB2) peaks at the cut.
13041
+ float u = second ? 2.0 - 2.0 * t : 2.0 * t;
13042
+ float travel = u * u * u;
13043
+ float speed = u * u;
13044
+ vec4 col;
13045
+ if (uMode == 0) {
13046
+ vec2 dir = vec2(cos(radians(uAngle)), sin(radians(uAngle))) * vec2(iResolution.y / iResolution.x, 1.0);
13047
+ // From leaves along dir; To arrives from behind, so it is offset back along dir.
13048
+ vec2 shift = dir * travel * (0.4 + 0.6 * uStrength) * (second ? -1.0 : 1.0);
13049
+ vec2 p = 0.5 + (uv - 0.5) / (1.0 + uPunch * speed) - shift;
13050
+ float len = uStrength * 0.6 * speed;
13051
+ col = second ? smear(uTo, p, dir, len) : smear(uFrom, p, dir, len);
13052
+ } else {
13053
+ vec2 p = 0.5 + (uv - 0.5) / (1.0 + 4.0 * uPunch * travel + 0.2 * uStrength * travel);
13054
+ float len = uStrength * 0.5 * speed;
13055
+ col = second ? zoomSmear(uTo, p, len) : zoomSmear(uFrom, p, len);
13056
+ }
13057
+ // A touch of exposure at the cut sells the speed.
13058
+ col.rgb *= 1.0 + 0.25 * uStrength * speed * speed;
13059
+ fragColor = col;
13060
+ }`
13061
+ ),
13062
+ presets: [
13063
+ { name: "Whip left", values: { uMode: 0, uAngle: 180 } },
13064
+ { name: "Whip up", values: { uMode: 0, uAngle: 90 } },
13065
+ { name: "Zoom punch", values: { uMode: 1, uStrength: 0.7, uPunch: 0.4 } },
13066
+ { name: "Hard whip", values: { uMode: 0, uAngle: 0, uStrength: 1, uPunch: 0.3 } }
13067
+ ]
13068
+ };
12111
13069
  var morphShaders = [
12112
13070
  featureMorph,
12113
13071
  silhouetteMorph,
@@ -12115,7 +13073,8 @@ var morphShaders = [
12115
13073
  transitionDisplacement,
12116
13074
  transitionLuma,
12117
13075
  transitionParticleSwap,
12118
- transitionInkBleed
13076
+ transitionInkBleed,
13077
+ transitionWhipPan
12119
13078
  ];
12120
13079
 
12121
13080
  // src/catalog/shaders/overlay.ts
@@ -20853,6 +21812,65 @@ var viewfinder = {
20853
21812
  ]).flat()
20854
21813
  }
20855
21814
  };
21815
+ var UPLOAD_IMAGE = { x: 40, y: 32, w: 160, h: 96 };
21816
+ var UPLOAD_BADGE = [184, 48];
21817
+ var UPLOAD_SHOW = 1900;
21818
+ var uploadAbout = (cx, cy, s) => `translate(${cx} ${cy}) scale(${s}) translate(${-cx} ${-cy})`;
21819
+ var num2 = (name) => `{{${name}}}`;
21820
+ var uploadCard = {
21821
+ kind: "vector",
21822
+ id: "upload-card",
21823
+ name: "Upload card",
21824
+ description: "An image upload card whose progress bar fills as the percentage counts to 100%, then the placeholder cross-fades into the finished picture with a check, and resets.",
21825
+ category: "loader",
21826
+ tags: ["upload", "progress", "image", "file", "ui", "loader"],
21827
+ hold: 300,
21828
+ presets: [
21829
+ { name: "Dark", values: { card: [0.09, 0.11, 0.15], ink: [0.95, 0.96, 0.97], accent: [0.55, 0.36, 0.96] } },
21830
+ { name: "Ocean", values: { label: "beach-day.png", accent: [0.02, 0.6, 0.75] } }
21831
+ ],
21832
+ derived: (v) => {
21833
+ const e = Math.max(0.6, textWidthEm(String(v.label)));
21834
+ const size = Math.round(Math.min(13, 112 / e) * 10) / 10;
21835
+ const up = Math.round(Math.min(6, Math.max(0.5, Number(v.duration) || 2.2)) * 1e3);
21836
+ const back = up + UPLOAD_SHOW;
21837
+ return { size, length: Math.round(e * size * 10) / 10, up, swap: up + 80, pop: up + 300, popped: up + 500, tickAt: up + 420, back, hidden: back + 300, shown: back + 350 };
21838
+ },
21839
+ motion: (() => {
21840
+ const { x, y, w, h } = UPLOAD_IMAGE;
21841
+ const [bx, by] = UPLOAD_BADGE;
21842
+ const mid = [x + w / 2, y + h / 2];
21843
+ return {
21844
+ svg: `<svg viewBox="0 0 240 200"><!-- {{label: text default='mountains.jpg' label='File name'}} {{duration: float min=0.5 max=6 default=2.2 step=0.1 label='Upload time (s)'}} {{card: color default=#FFFFFF label='Card'}} {{accent: color default=#6366F1 label='Accent'}} {{ink: color default=#111827 label='Text colour'}} --><defs><filter id="cardShadow" x="-20%" y="-20%" width="140%" height="150%"><feGaussianBlur stdDeviation="6"/></filter><clipPath id="imageClip"><rect x="${x}" y="${y}" width="${w}" height="${h}" rx="10"/></clipPath><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.45"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0.1"/></linearGradient></defs><rect x="24" y="22" width="192" height="168" rx="16" fill="#000000" opacity="0.14" filter="url(#cardShadow)"/><rect x="24" y="16" width="192" height="168" rx="16" fill="{{card}}" stroke="{{ink}}" stroke-opacity="0.08"/><g id="placeholder"><rect x="${x}" y="${y}" width="${w}" height="${h}" rx="10" fill="{{ink}}" fill-opacity="0.05" stroke="{{ink}}" stroke-opacity="0.18" stroke-width="1.5" stroke-dasharray="5 4"/><g fill="none" stroke="{{ink}}" stroke-opacity="0.4" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="142" cy="64" r="7"/><path d="M84 104 L104 80 L118 95 L128 86 L156 104 Z"/></g></g><g id="picture" opacity="0" transform="${uploadAbout(mid[0], mid[1], 1.06)}" clip-path="url(#imageClip)"><rect x="${x}" y="${y}" width="${w}" height="${h}" fill="{{card}}"/><rect x="${x}" y="${y}" width="${w}" height="${h}" fill="url(#sky)"/><circle cx="150" cy="60" r="11" fill="#FDE68A"/><path d="M40 128 L40 110 L82 74 L114 102 L146 80 L200 116 L200 128 Z" fill="{{accent}}" fill-opacity="0.55"/><path d="M40 128 L40 118 Q90 96 138 110 T200 106 L200 128 Z" fill="{{accent}}"/></g><g id="badge" transform="${uploadAbout(bx, by, 0)}"><circle cx="${bx}" cy="${by}" r="11" fill="{{accent}}" stroke="{{card}}" stroke-width="3"/><path id="tick" d="M${bx - 5} ${by + 0.5} L${bx - 1.5} ${by + 4} L${bx + 5} ${by - 3.5}" fill="none" stroke="#FFFFFF" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g><text x="${x}" y="152" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{label}}</text><text id="pct" x="${x + w}" y="152" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="12" font-weight="500" fill="{{ink}}" fill-opacity="0.6">0%</text><rect x="${x}" y="162" width="${w}" height="6" rx="3" fill="{{ink}}" fill-opacity="0.1"/><rect id="bar" x="${x}" y="162" width="0" height="6" rx="3" fill="{{accent}}"/></svg>`,
21845
+ animate: [
21846
+ // Uploading: the bar and the percentage climb together.
21847
+ { id: "bar", attrs: { width: [0, w] }, ms: num2("up"), ease: "in-out" },
21848
+ { id: "pct", count: [0, 100], format: "{n}%", ms: num2("up"), ease: "in-out" },
21849
+ // Done: the placeholder gives way to the picture, settling as it fades in, and a check pops on.
21850
+ { id: "placeholder", attrs: { opacity: [1, 0] }, delay: num2("swap"), ms: 360 },
21851
+ { id: "picture", attrs: { opacity: [0, 1] }, delay: num2("swap"), ms: 420 },
21852
+ { id: "picture", attrs: { transform: [uploadAbout(mid[0], mid[1], 1.06), uploadAbout(mid[0], mid[1], 1)] }, delay: num2("swap"), ms: 600, ease: "out" },
21853
+ { id: "badge", attrs: { transform: [uploadAbout(bx, by, 0), uploadAbout(bx, by, 1.2)] }, delay: num2("pop"), ms: 200, ease: "out" },
21854
+ { id: "badge", attrs: { transform: [uploadAbout(bx, by, 1.2), uploadAbout(bx, by, 1)] }, delay: num2("popped"), ms: 160, ease: "in-out" },
21855
+ { id: "tick", attrs: { opacity: [0, 1] }, delay: num2("tickAt"), ms: 40 },
21856
+ { id: "tick", draw: true, delay: num2("tickAt"), ms: 260, ease: "out" },
21857
+ // Reset: back to the empty placeholder, the bar and count fading out and back in at zero.
21858
+ { id: "picture", attrs: { opacity: [1, 0] }, delay: num2("back"), ms: 360 },
21859
+ { id: "placeholder", attrs: { opacity: [0, 1] }, delay: num2("back"), ms: 360 },
21860
+ { id: "badge", attrs: { transform: [uploadAbout(bx, by, 1), uploadAbout(bx, by, 0)] }, delay: num2("back"), ms: 220, ease: "in-out" },
21861
+ { id: "tick", attrs: { opacity: [1, 0] }, delay: num2("back"), ms: 120 },
21862
+ { id: "tick", draw: [0, 0], delay: num2("hidden"), ms: 0 },
21863
+ { id: "picture", attrs: { transform: [uploadAbout(mid[0], mid[1], 1.06), uploadAbout(mid[0], mid[1], 1.06)] }, delay: num2("hidden"), ms: 0 },
21864
+ { id: "bar", attrs: { opacity: [1, 0] }, delay: num2("back"), ms: 250 },
21865
+ { id: "pct", attrs: { opacity: [1, 0] }, delay: num2("back"), ms: 250 },
21866
+ { id: "bar", attrs: { width: [0, 0] }, delay: num2("hidden"), ms: 0 },
21867
+ { id: "pct", count: [0, 0], format: "{n}%", delay: num2("hidden"), ms: 0 },
21868
+ { id: "bar", attrs: { opacity: [0, 1] }, delay: num2("shown"), ms: 250 },
21869
+ { id: "pct", attrs: { opacity: [0, 1] }, delay: num2("shown"), ms: 250 }
21870
+ ]
21871
+ };
21872
+ })()
21873
+ };
20856
21874
  var vectorEffects = [
20857
21875
  spinner,
20858
21876
  bouncingDots,
@@ -20872,7 +21890,8 @@ var vectorEffects = [
20872
21890
  blobMorph,
20873
21891
  dripText,
20874
21892
  flowNetwork,
20875
- viewfinder
21893
+ viewfinder,
21894
+ uploadCard
20876
21895
  ];
20877
21896
 
20878
21897
  // src/catalog/index.ts
@@ -20895,6 +21914,8 @@ var SHADER_EFFECTS = [
20895
21914
  moire,
20896
21915
  kaleidoscope,
20897
21916
  barberStripes,
21917
+ memphisPattern,
21918
+ chevronTunnel,
20898
21919
  neonCircuit,
20899
21920
  bokeh,
20900
21921
  godRays,
@@ -21017,4 +22038,4 @@ export {
21017
22038
  effectControls,
21018
22039
  effectDefaults
21019
22040
  };
21020
- //# sourceMappingURL=chunk-PYDN5G2P.js.map
22041
+ //# sourceMappingURL=chunk-DBNSI4MF.js.map