@pixerate/flair 0.9.0 → 0.10.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.
- package/CHANGELOG.md +10 -0
- package/README.md +8 -4
- package/dist/catalog/index.d.ts +3 -3
- package/dist/catalog/index.js +1 -1
- package/dist/{chunk-EQS4EXNM.js → chunk-SH66CNLC.js} +52 -6
- package/dist/chunk-SH66CNLC.js.map +1 -0
- package/dist/{chunk-F754SFRY.js → chunk-STGGN4MR.js} +1198 -115
- package/dist/chunk-STGGN4MR.js.map +1 -0
- package/dist/{chunk-TDMD5XAU.js → chunk-YACVME3D.js} +75 -4
- package/dist/{chunk-TDMD5XAU.js.map → chunk-YACVME3D.js.map} +1 -1
- package/dist/{chunk-EZRRKQFV.js → chunk-YLJWC4N2.js} +96 -1
- package/dist/chunk-YLJWC4N2.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/editor/index.d.ts +3 -4
- package/dist/editor/index.js +4 -4
- package/dist/editor/index.js.map +1 -1
- package/dist/index.d.ts +65 -6
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/{renderer-0_Ap1tyi.d.ts → renderer-SNYZu__R.d.ts} +17 -0
- package/dist/svg-CpeanaE_.d.ts +375 -0
- package/dist/{types-CgHG5JLv.d.ts → types-D8uhzDyN.d.ts} +8 -2
- package/dist/vector/index.d.ts +2 -120
- package/dist/vector/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-EQS4EXNM.js.map +0 -1
- package/dist/chunk-EZRRKQFV.js.map +0 -1
- package/dist/chunk-F754SFRY.js.map +0 -1
- package/dist/clock-BhdXWH-n.d.ts +0 -47
- package/dist/motion-Ce0lmzm3.d.ts +0 -179
|
@@ -1465,14 +1465,535 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
|
1465
1465
|
]
|
|
1466
1466
|
};
|
|
1467
1467
|
|
|
1468
|
+
// src/catalog/interaction.ts
|
|
1469
|
+
var r1 = (v) => Math.round(v * 10) / 10;
|
|
1470
|
+
var r2 = (v) => Math.round(v * 100) / 100;
|
|
1471
|
+
var n = (name) => `{{${name}}}`;
|
|
1472
|
+
var mixHex = (a, b, t) => {
|
|
1473
|
+
const x = Array.isArray(a) ? a : [1, 1, 1];
|
|
1474
|
+
const y = Array.isArray(b) ? b : [1, 1, 1];
|
|
1475
|
+
return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
|
|
1476
|
+
};
|
|
1477
|
+
function fit(text, width, max) {
|
|
1478
|
+
const e = Math.max(0.6, textWidthEm(String(text)));
|
|
1479
|
+
const size = Math.min(max, width / e);
|
|
1480
|
+
return { size: r1(size), length: r1(e * size) };
|
|
1481
|
+
}
|
|
1482
|
+
var about = (cx, cy, s) => `translate(${cx} ${cy}) scale(${r2(s)}) translate(${-cx} ${-cy})`;
|
|
1483
|
+
var snap = (id, attr, value, delay, step = 1) => ({
|
|
1484
|
+
id,
|
|
1485
|
+
step,
|
|
1486
|
+
attrs: { [attr]: [value, value] },
|
|
1487
|
+
delay,
|
|
1488
|
+
ms: 0
|
|
1489
|
+
});
|
|
1490
|
+
var SANS = `system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif`;
|
|
1491
|
+
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";
|
|
1492
|
+
var PARTICLES = 8;
|
|
1493
|
+
var BURST_FROM = 22;
|
|
1494
|
+
var BURST_TO = 46;
|
|
1495
|
+
var HEART_X = 72;
|
|
1496
|
+
var HEART_Y = 60;
|
|
1497
|
+
var heartPose = (k) => `translate(${HEART_X} ${HEART_Y}) scale(${r2(2.2 * k)}) translate(-12 -12)`;
|
|
1498
|
+
var likeButton = {
|
|
1499
|
+
kind: "vector",
|
|
1500
|
+
id: "like-button",
|
|
1501
|
+
name: "Like button",
|
|
1502
|
+
description: "A heart that fills with a springy pop, a ring and a burst of particles as its like count ticks up, and empties again on the next click.",
|
|
1503
|
+
category: "interaction",
|
|
1504
|
+
tags: ["interactive", "ui", "click", "like", "heart", "toggle", "social", "micro-interaction"],
|
|
1505
|
+
trigger: "click",
|
|
1506
|
+
hold: 900,
|
|
1507
|
+
presets: [
|
|
1508
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], chip: [0.13, 0.14, 0.19], outline: [0.6, 0.63, 0.7], ink: [0.9, 0.91, 0.95] } },
|
|
1509
|
+
{ name: "Purple", values: { heart: [0.6, 0.3, 1], accent: [1, 0.4, 0.75], count: 1204 } }
|
|
1510
|
+
],
|
|
1511
|
+
derived: (v) => ({ liked: Number(v.count) + 1 }),
|
|
1512
|
+
motion: {
|
|
1513
|
+
svg: `<svg viewBox="0 0 240 120"><!-- {{count: int min=0 max=99999 default=42 label='Start count'}} {{background: color default=#FFFFFF label='Background'}} {{chip: color default=#F3F4F6 label='Button'}} {{outline: color default=#6B7280 label='Outline'}} {{heart: color default=#F43F5E label='Heart'}} {{accent: color default=#FB923C label='Sparks'}} {{ink: color default=#111827 label='Count colour'}} --><rect width="240" height="120" fill="{{background}}"/><rect x="20" y="24" width="200" height="72" rx="36" fill="{{chip}}"/><circle id="ring" cx="${HEART_X}" cy="${HEART_Y}" r="14" fill="none" stroke="{{heart}}" stroke-width="0"/>${Array.from(
|
|
1514
|
+
{ length: PARTICLES },
|
|
1515
|
+
(_, k) => {
|
|
1516
|
+
const a = (k * 360 / PARTICLES - 90) * (Math.PI / 180);
|
|
1517
|
+
return `<circle id="p${k}" cx="${r1(HEART_X + BURST_FROM * Math.cos(a))}" cy="${r1(HEART_Y + BURST_FROM * Math.sin(a))}" r="0" fill="${k % 2 ? "{{accent}}" : "{{heart}}"}"/>`;
|
|
1518
|
+
}
|
|
1519
|
+
).join("")}<g id="hg" transform="${heartPose(1)}"><path id="heartShape" d="${HEART2}" fill="{{heart}}" fill-opacity="0" stroke="{{outline}}" stroke-width="1.8" stroke-linejoin="round"/></g><text id="tally" x="106" y="70.5" font-family="${SANS}" font-size="30" font-weight="700" fill="{{ink}}">{{count}}</text></svg>`,
|
|
1520
|
+
animate: [
|
|
1521
|
+
// Step 1, like: squash, pop past full size and settle while it fills.
|
|
1522
|
+
{ id: "hg", step: 1, attrs: { transform: [heartPose(1), heartPose(0.78)] }, ms: 110, ease: "out" },
|
|
1523
|
+
{ id: "hg", step: 1, attrs: { transform: [heartPose(0.78), heartPose(1.22)] }, delay: 110, ms: 170, ease: "out" },
|
|
1524
|
+
{ id: "hg", step: 1, attrs: { transform: [heartPose(1.22), heartPose(1)] }, delay: 280, ms: 220, ease: "in-out" },
|
|
1525
|
+
{ id: "heartShape", step: 1, attrs: { "fill-opacity": [0, 1], stroke: ["{{outline}}", "{{heart}}"] }, delay: 110, ms: 120 },
|
|
1526
|
+
// The ring and sparks burst out, then snap back once they are gone.
|
|
1527
|
+
snap("ring", "stroke-width", 0, 0),
|
|
1528
|
+
{ id: "ring", step: 1, attrs: { r: [14, 44], "stroke-width": [10, 0] }, delay: 130, ms: 480, ease: "out" },
|
|
1529
|
+
snap("ring", "r", 14, 700),
|
|
1530
|
+
...Array.from({ length: PARTICLES }, (_, k) => {
|
|
1531
|
+
const a = (k * 360 / PARTICLES - 90) * (Math.PI / 180);
|
|
1532
|
+
const [x0, y0] = [r1(HEART_X + BURST_FROM * Math.cos(a)), r1(HEART_Y + BURST_FROM * Math.sin(a))];
|
|
1533
|
+
const [x1, y1] = [r1(HEART_X + BURST_TO * Math.cos(a)), r1(HEART_Y + BURST_TO * Math.sin(a))];
|
|
1534
|
+
return [
|
|
1535
|
+
snap(`p${k}`, "r", 0, 0),
|
|
1536
|
+
{ id: `p${k}`, step: 1, attrs: { cx: [x0, x1], cy: [y0, y1], r: [4.5, 0] }, delay: 150, ms: 520, ease: "out" },
|
|
1537
|
+
snap(`p${k}`, "cx", x0, 700),
|
|
1538
|
+
snap(`p${k}`, "cy", y0, 700)
|
|
1539
|
+
];
|
|
1540
|
+
}).flat(),
|
|
1541
|
+
{ id: "tally", step: 1, count: [n("count"), n("liked")], attrs: { fill: ["{{ink}}", "{{heart}}"] }, delay: 150, ms: 260, ease: "out" },
|
|
1542
|
+
// Step 2, unlike: a small dip as it empties and the count goes back.
|
|
1543
|
+
{ id: "hg", step: 2, attrs: { transform: [heartPose(1), heartPose(0.86)] }, ms: 120, ease: "out" },
|
|
1544
|
+
{ id: "hg", step: 2, attrs: { transform: [heartPose(0.86), heartPose(1)] }, delay: 120, ms: 200, ease: "in-out" },
|
|
1545
|
+
{ id: "heartShape", step: 2, attrs: { "fill-opacity": [1, 0], stroke: ["{{heart}}", "{{outline}}"] }, ms: 180 },
|
|
1546
|
+
{ id: "tally", step: 2, count: [n("liked"), n("count")], attrs: { fill: ["{{heart}}", "{{ink}}"] }, ms: 220, ease: "out" }
|
|
1547
|
+
]
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
var toggleSwitch = {
|
|
1551
|
+
kind: "vector",
|
|
1552
|
+
id: "toggle-switch",
|
|
1553
|
+
name: "Toggle switch",
|
|
1554
|
+
description: "A switch whose knob slides across, squashing a little mid-slide, as the track blends to its on colour; the next click slides it back.",
|
|
1555
|
+
category: "interaction",
|
|
1556
|
+
tags: ["interactive", "ui", "click", "toggle", "switch", "settings", "micro-interaction"],
|
|
1557
|
+
trigger: "click",
|
|
1558
|
+
hold: 900,
|
|
1559
|
+
presets: [
|
|
1560
|
+
{ name: "iOS", values: { on: [0.2, 0.78, 0.35] } },
|
|
1561
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], off: [0.22, 0.24, 0.3], on: [0.55, 0.45, 1], knob: [0.96, 0.96, 1] } }
|
|
1562
|
+
],
|
|
1563
|
+
motion: (() => {
|
|
1564
|
+
const slide = (from, to, step) => [
|
|
1565
|
+
{ id: "slide", step, attrs: { transform: [`translate(${from} 0)`, `translate(${to} 0)`] }, ms: 320, ease: "in-out" },
|
|
1566
|
+
// Stretched along the slide and flattened halfway, round again at the end.
|
|
1567
|
+
{ id: "knob", step, attrs: { rx: [19, 24], ry: [19, 16.5] }, ms: 160, ease: "out" },
|
|
1568
|
+
{ id: "knob", step, attrs: { rx: [24, 19], ry: [16.5, 19] }, delay: 160, ms: 160, ease: "in-out" }
|
|
1569
|
+
];
|
|
1570
|
+
return {
|
|
1571
|
+
svg: `<svg viewBox="0 0 200 120"><!-- {{background: color default=#FFFFFF label='Background'}} {{off: color default=#D1D5DB label='Track off'}} {{on: color default=#6366F1 label='Track on'}} {{knob: color default=#FFFFFF label='Knob'}} --><rect width="200" height="120" fill="{{background}}"/><rect id="track" x="40" y="35" width="120" height="50" rx="25" fill="{{off}}"/><g id="slide" transform="translate(0 0)"><ellipse cx="65" cy="62.5" rx="18" ry="18" fill="#000000" opacity="0.16"/><ellipse id="knob" cx="65" cy="60" rx="19" ry="19" fill="{{knob}}"/></g></svg>`,
|
|
1572
|
+
animate: [
|
|
1573
|
+
...slide(0, 70, 1),
|
|
1574
|
+
{ id: "track", step: 1, attrs: { fill: ["{{off}}", "{{on}}"] }, ms: 320, ease: "in-out" },
|
|
1575
|
+
...slide(70, 0, 2),
|
|
1576
|
+
{ id: "track", step: 2, attrs: { fill: ["{{on}}", "{{off}}"] }, ms: 320, ease: "in-out" }
|
|
1577
|
+
]
|
|
1578
|
+
};
|
|
1579
|
+
})()
|
|
1580
|
+
};
|
|
1581
|
+
var checkbox = {
|
|
1582
|
+
kind: "vector",
|
|
1583
|
+
id: "checkbox",
|
|
1584
|
+
name: "Checkbox",
|
|
1585
|
+
description: "A labelled checkbox that fills with a press and draws its tick on, then undraws it and empties on the next click.",
|
|
1586
|
+
category: "interaction",
|
|
1587
|
+
tags: ["interactive", "ui", "click", "checkbox", "form", "toggle", "todo", "micro-interaction"],
|
|
1588
|
+
trigger: "click",
|
|
1589
|
+
hold: 900,
|
|
1590
|
+
presets: [
|
|
1591
|
+
{ name: "Green", values: { accent: [0.13, 0.7, 0.4] } },
|
|
1592
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], border: [0.4, 0.43, 0.5], ink: [0.92, 0.93, 0.96], accent: [0.95, 0.6, 0.15], tick: [0.06, 0.07, 0.1] } }
|
|
1593
|
+
],
|
|
1594
|
+
derived: (v) => {
|
|
1595
|
+
const { size, length } = fit(v.label, 216, 24);
|
|
1596
|
+
return { size, length, base: r1(60 + size * 0.35) };
|
|
1597
|
+
},
|
|
1598
|
+
motion: {
|
|
1599
|
+
svg: `<svg viewBox="0 0 320 120"><!-- {{label: text default='Remember me' label='Label'}} {{background: color default=#FFFFFF label='Background'}} {{border: color default=#9CA3AF label='Border'}} {{accent: color default=#6366F1 label='Checked'}} {{tick: color default=#FFFFFF label='Tick'}} {{ink: color default=#111827 label='Label colour'}} --><rect width="320" height="120" fill="{{background}}"/><g id="boxg" transform="${about(48, 60, 1)}"><rect id="box" x="30" y="42" width="36" height="36" rx="9" fill="{{accent}}" fill-opacity="0" stroke="{{border}}" stroke-width="3"/><path id="tick" d="M39 60.5 L45.5 67 L58 53.5" fill="none" stroke="{{tick}}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g><text x="84" y="{{base}}" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="500" fill="{{ink}}">{{label}}</text></svg>`,
|
|
1600
|
+
animate: [
|
|
1601
|
+
// Step 1, check: the box dips as it fills, then the tick draws on.
|
|
1602
|
+
{ id: "boxg", step: 1, attrs: { transform: [about(48, 60, 1), about(48, 60, 0.86)] }, ms: 100, ease: "out" },
|
|
1603
|
+
{ id: "boxg", step: 1, attrs: { transform: [about(48, 60, 0.86), about(48, 60, 1)] }, delay: 100, ms: 220, ease: "in-out" },
|
|
1604
|
+
{ id: "box", step: 1, attrs: { "fill-opacity": [0, 1], stroke: ["{{border}}", "{{accent}}"] }, ms: 160 },
|
|
1605
|
+
// Shown only once it starts drawing: a round cap would leave a dot.
|
|
1606
|
+
{ id: "tick", step: 1, attrs: { opacity: [0, 1] }, delay: 120, ms: 40 },
|
|
1607
|
+
{ id: "tick", step: 1, draw: true, delay: 120, ms: 260, ease: "out" },
|
|
1608
|
+
// Step 2, uncheck: the tick undraws, then the box empties.
|
|
1609
|
+
{ id: "tick", step: 2, draw: [1, 0], ms: 160, ease: "in-out" },
|
|
1610
|
+
{ id: "tick", step: 2, attrs: { opacity: [1, 0] }, delay: 140, ms: 30 },
|
|
1611
|
+
{ id: "box", step: 2, attrs: { "fill-opacity": [1, 0], stroke: ["{{accent}}", "{{border}}"] }, delay: 100, ms: 180 },
|
|
1612
|
+
{ id: "boxg", step: 2, attrs: { transform: [about(48, 60, 1), about(48, 60, 0.92)] }, delay: 100, ms: 90, ease: "out" },
|
|
1613
|
+
{ id: "boxg", step: 2, attrs: { transform: [about(48, 60, 0.92), about(48, 60, 1)] }, delay: 190, ms: 160, ease: "in-out" }
|
|
1614
|
+
]
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
function capsule(p, q, width, flatEnd = false) {
|
|
1618
|
+
const h = width / 2;
|
|
1619
|
+
const a = Math.atan2(q[1] - p[1], q[0] - p[0]);
|
|
1620
|
+
const pts = [];
|
|
1621
|
+
const cap = (c, from) => {
|
|
1622
|
+
for (let k = 0; k <= 8; k++) {
|
|
1623
|
+
const t = from + k * Math.PI / 8;
|
|
1624
|
+
pts.push([c[0] + h * Math.cos(t), c[1] + h * Math.sin(t)]);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
if (flatEnd) pts.push([q[0] + h * Math.cos(a - Math.PI / 2), q[1] + h * Math.sin(a - Math.PI / 2)], [q[0] + h * Math.cos(a + Math.PI / 2), q[1] + h * Math.sin(a + Math.PI / 2)]);
|
|
1628
|
+
else cap(q, a - Math.PI / 2);
|
|
1629
|
+
cap(p, a + Math.PI / 2);
|
|
1630
|
+
return `M${pts.map((pt) => `${r2(pt[0])} ${r2(pt[1])}`).join(" L")} Z`;
|
|
1631
|
+
}
|
|
1632
|
+
var menuMorphs = /* @__PURE__ */ new Map();
|
|
1633
|
+
function menuMorph(weight) {
|
|
1634
|
+
if (!menuMorphs.has(weight)) {
|
|
1635
|
+
const bars = [7, 12, 17].map((y) => capsule([5, y], [19, y], weight)).join(" ");
|
|
1636
|
+
const meet = (weight / 2 + 0.05) / Math.SQRT2;
|
|
1637
|
+
const cross = [
|
|
1638
|
+
capsule([6.5, 6.5], [17.5, 17.5], weight),
|
|
1639
|
+
capsule([17.5, 6.5], [12 + meet, 12 - meet], weight, true),
|
|
1640
|
+
capsule([6.5, 17.5], [12 - meet, 12 + meet], weight, true)
|
|
1641
|
+
].join(" ");
|
|
1642
|
+
menuMorphs.set(weight, morphPaths(bars, cross, { points: 48 }));
|
|
1643
|
+
}
|
|
1644
|
+
return menuMorphs.get(weight);
|
|
1645
|
+
}
|
|
1646
|
+
var menuPose = (turn) => `translate(60 60) rotate(${turn}) scale(3) translate(-12 -12)`;
|
|
1647
|
+
var menuToggle = {
|
|
1648
|
+
kind: "vector",
|
|
1649
|
+
id: "menu-toggle",
|
|
1650
|
+
name: "Menu toggle",
|
|
1651
|
+
description: "A hamburger menu button whose three bars morph into a close cross as it spins half a turn, and back on the next click.",
|
|
1652
|
+
category: "interaction",
|
|
1653
|
+
tags: ["interactive", "ui", "click", "menu", "hamburger", "close", "morph", "navigation", "micro-interaction"],
|
|
1654
|
+
trigger: "click",
|
|
1655
|
+
hold: 900,
|
|
1656
|
+
presets: [
|
|
1657
|
+
{ name: "Light", values: { background: [1, 1, 1], button: [0.95, 0.96, 0.97], active: [0.07, 0.09, 0.15], ink: [0.07, 0.09, 0.15], activeInk: [1, 1, 1] } },
|
|
1658
|
+
{ name: "Bold", values: { weight: 3, button: [0.98, 0.8, 0.1], active: [0.95, 0.3, 0.2], ink: [0.1, 0.1, 0.1], activeInk: [1, 1, 1] } }
|
|
1659
|
+
],
|
|
1660
|
+
derived: (v) => {
|
|
1661
|
+
const [bars, cross] = menuMorph(Math.round(Number(v.weight) * 4) / 4);
|
|
1662
|
+
return { bars, cross };
|
|
1663
|
+
},
|
|
1664
|
+
motion: {
|
|
1665
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{weight: float min=1 max=3.5 default=2 step=0.25 label='Line weight'}} {{background: color default=#0F172A label='Background'}} {{button: color default=#1E293B label='Button'}} {{active: color default=#6366F1 label='Button open'}} {{ink: color default=#F8FAFC label='Icon'}} {{activeInk: color default=#FFFFFF label='Icon open'}} --><rect width="120" height="120" fill="{{background}}"/><rect id="bg" x="16" y="16" width="88" height="88" rx="24" fill="{{button}}"/><g id="icon" transform="${menuPose(0)}"><path id="glyph" d="{{bars}}" fill="{{ink}}" fill-rule="evenodd"/></g></svg>`,
|
|
1666
|
+
animate: [
|
|
1667
|
+
// Step 1, open: bars to cross while turning half a turn.
|
|
1668
|
+
{ id: "glyph", step: 1, attrs: { d: ["{{bars}}", "{{cross}}"], fill: ["{{ink}}", "{{activeInk}}"] }, ms: 450, ease: "in-out" },
|
|
1669
|
+
{ id: "icon", step: 1, attrs: { transform: [menuPose(0), menuPose(180)] }, ms: 450, ease: "in-out" },
|
|
1670
|
+
{ id: "bg", step: 1, attrs: { fill: ["{{button}}", "{{active}}"] }, ms: 300, ease: "in-out" },
|
|
1671
|
+
// Step 2, close: on round the same way (-180° looks like 180°), ending where it started.
|
|
1672
|
+
{ id: "glyph", step: 2, attrs: { d: ["{{cross}}", "{{bars}}"], fill: ["{{activeInk}}", "{{ink}}"] }, ms: 450, ease: "in-out" },
|
|
1673
|
+
{ id: "icon", step: 2, attrs: { transform: [menuPose(-180), menuPose(0)] }, ms: 450, ease: "in-out" },
|
|
1674
|
+
{ id: "bg", step: 2, attrs: { fill: ["{{active}}", "{{button}}"] }, ms: 300, ease: "in-out" }
|
|
1675
|
+
]
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var BOOKMARK = "M41 22 H79 Q84 22 84 27 V98 L60 81 L36 98 V27 Q36 22 41 22 Z";
|
|
1679
|
+
var bounce = (y) => `translate(0 ${y})`;
|
|
1680
|
+
var bookmark = {
|
|
1681
|
+
kind: "vector",
|
|
1682
|
+
id: "bookmark",
|
|
1683
|
+
name: "Bookmark",
|
|
1684
|
+
description: "A bookmark that hops and fills from top to bottom when saved, then drains and dips when unsaved.",
|
|
1685
|
+
category: "interaction",
|
|
1686
|
+
tags: ["interactive", "ui", "click", "bookmark", "save", "toggle", "micro-interaction"],
|
|
1687
|
+
trigger: "click",
|
|
1688
|
+
hold: 900,
|
|
1689
|
+
presets: [
|
|
1690
|
+
{ name: "Gold", values: { fill: [0.98, 0.72, 0.1] } },
|
|
1691
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], outline: [0.6, 0.63, 0.7], fill: [0.4, 0.8, 1] } }
|
|
1692
|
+
],
|
|
1693
|
+
motion: {
|
|
1694
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{background: color default=#FFFFFF label='Background'}} {{outline: color default=#6B7280 label='Outline'}} {{fill: color default=#6366F1 label='Saved'}} --><defs><clipPath id="fillClip"><rect id="level" x="30" y="18" width="60" height="0"/></clipPath></defs><rect width="120" height="120" fill="{{background}}"/><g id="mark" transform="${bounce(0)}"><path d="${BOOKMARK}" fill="{{fill}}" clip-path="url(#fillClip)"/><path id="edge" d="${BOOKMARK}" fill="none" stroke="{{outline}}" stroke-width="4" stroke-linejoin="round"/></g></svg>`,
|
|
1695
|
+
animate: [
|
|
1696
|
+
// Step 1, save: a hop, a drop past rest and a settle, filling top down.
|
|
1697
|
+
{ id: "mark", step: 1, attrs: { transform: [bounce(0), bounce(-7)] }, ms: 130, ease: "out" },
|
|
1698
|
+
{ id: "mark", step: 1, attrs: { transform: [bounce(-7), bounce(4)] }, delay: 130, ms: 170, ease: "in-out" },
|
|
1699
|
+
{ id: "mark", step: 1, attrs: { transform: [bounce(4), bounce(0)] }, delay: 300, ms: 200, ease: "in-out" },
|
|
1700
|
+
{ id: "level", step: 1, attrs: { height: [0, 84] }, delay: 60, ms: 380, ease: "in-out" },
|
|
1701
|
+
{ id: "edge", step: 1, attrs: { stroke: ["{{outline}}", "{{fill}}"] }, ms: 200 },
|
|
1702
|
+
// Step 2, unsave: drains top down (then the empty clip snaps back up) with a small dip.
|
|
1703
|
+
{ id: "level", step: 2, attrs: { y: [18, 102], height: [84, 0] }, ms: 320, ease: "in-out" },
|
|
1704
|
+
snap("level", "y", 18, 320, 2),
|
|
1705
|
+
{ id: "mark", step: 2, attrs: { transform: [bounce(0), bounce(3)] }, ms: 120, ease: "out" },
|
|
1706
|
+
{ id: "mark", step: 2, attrs: { transform: [bounce(3), bounce(0)] }, delay: 120, ms: 180, ease: "in-out" },
|
|
1707
|
+
{ id: "edge", step: 2, attrs: { stroke: ["{{fill}}", "{{outline}}"] }, delay: 100, ms: 200 }
|
|
1708
|
+
]
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
var swapLabels = (outAt, back) => [
|
|
1712
|
+
{ id: "label", step: 1, attrs: { opacity: [1, 0], transform: ["translate(0 0)", "translate(0 -10)"] }, delay: outAt, ms: 180, ease: "out" },
|
|
1713
|
+
{ id: "done", step: 1, attrs: { opacity: [0, 1], transform: ["translate(0 10)", "translate(0 0)"] }, delay: outAt + 120, ms: 220, ease: "out" },
|
|
1714
|
+
{ id: "done", step: 1, attrs: { opacity: [1, 0], transform: ["translate(0 0)", "translate(0 -10)"] }, delay: back, ms: 180, ease: "out" },
|
|
1715
|
+
{ id: "label", step: 1, attrs: { opacity: [0, 1], transform: ["translate(0 10)", "translate(0 0)"] }, delay: back + 120, ms: 220, ease: "out" },
|
|
1716
|
+
snap("done", "transform", "translate(0 10)", back + 200)
|
|
1717
|
+
];
|
|
1718
|
+
var checkFlash = (at, back) => [
|
|
1719
|
+
{ id: "check", step: 1, attrs: { opacity: [0, 1] }, delay: at, ms: 40 },
|
|
1720
|
+
{ id: "check", step: 1, draw: true, delay: at, ms: 280, ease: "out" },
|
|
1721
|
+
{ id: "check", step: 1, attrs: { opacity: [1, 0] }, delay: back, ms: 160 },
|
|
1722
|
+
{ id: "check", step: 1, draw: [0, 0], delay: back + 200, ms: 0 }
|
|
1723
|
+
];
|
|
1724
|
+
var buttonLabels = (a, b, room, max) => {
|
|
1725
|
+
const ems = [a, b].map((t) => Math.max(0.6, textWidthEm(String(t))));
|
|
1726
|
+
const size = Math.min(max, room / Math.max(...ems));
|
|
1727
|
+
return { size: r1(size), base: r1(50 + size * 0.35), labelLen: r1(ems[0] * size), doneLen: r1(ems[1] * size) };
|
|
1728
|
+
};
|
|
1729
|
+
var labelTexts = (x) => `<text id="label" x="${x}" y="{{base}}" textLength="{{labelLen}}" lengthAdjust="spacingAndGlyphs" transform="translate(0 0)" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{label}}</text><text id="done" x="${x}" y="{{base}}" textLength="{{doneLen}}" lengthAdjust="spacingAndGlyphs" transform="translate(0 10)" opacity="0" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{doneText}}</text>`;
|
|
1730
|
+
var COPY_BACK = 1600;
|
|
1731
|
+
var copyPose = (s) => `translate(54 50) scale(${r2(1.2 * s)}) translate(-12 -12)`;
|
|
1732
|
+
var copyButton = {
|
|
1733
|
+
kind: "vector",
|
|
1734
|
+
id: "copy-button",
|
|
1735
|
+
name: "Copy button",
|
|
1736
|
+
description: "A copy button whose icon shrinks away for a drawn check and a \u201CCopied!\u201D label, turning back by itself a moment later.",
|
|
1737
|
+
category: "interaction",
|
|
1738
|
+
tags: ["interactive", "ui", "click", "copy", "clipboard", "feedback", "button", "micro-interaction"],
|
|
1739
|
+
trigger: "click",
|
|
1740
|
+
hold: 600,
|
|
1741
|
+
presets: [
|
|
1742
|
+
{ name: "Light", values: { background: [1, 1, 1], button: [0.95, 0.96, 0.97], success: [0.86, 0.97, 0.9], ink: [0.07, 0.09, 0.15] } },
|
|
1743
|
+
{ name: "Code", values: { label: "Copy code", doneText: "Copied to clipboard", button: [0.2, 0.22, 0.28] } }
|
|
1744
|
+
],
|
|
1745
|
+
derived: (v) => buttonLabels(v.label, v.doneText, 126, 19),
|
|
1746
|
+
motion: {
|
|
1747
|
+
svg: `<svg viewBox="0 0 240 100"><!-- {{label: text default='Copy' label='Label'}} {{doneText: text default='Copied!' label='Copied label'}} {{background: color default=#0F172A label='Background'}} {{button: color default=#1E293B label='Button'}} {{success: color default=#166534 label='Copied'}} {{ink: color default=#F8FAFC label='Text and icon'}} --><rect width="240" height="100" fill="{{background}}"/><rect id="btn" x="20" y="24" width="200" height="52" rx="14" fill="{{button}}"/><g id="copy" transform="${copyPose(1)}" opacity="1" fill="none" stroke="{{ink}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="11.5" height="11.5" rx="2.5"/><path d="M4.5 15.5 V6.5 C4.5 5.4 5.4 4.5 6.5 4.5 H15.5"/></g><g transform="${copyPose(1)}"><path id="check" d="M5 12.5 L10 17.5 L19 7" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g>${labelTexts(80)}</svg>`,
|
|
1748
|
+
animate: [
|
|
1749
|
+
// One step that returns by itself: each click replays it.
|
|
1750
|
+
{ id: "copy", step: 1, attrs: { transform: [copyPose(1), copyPose(0.3)], opacity: [1, 0] }, ms: 180, ease: "out" },
|
|
1751
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{button}}", "{{success}}"] }, ms: 200 },
|
|
1752
|
+
...checkFlash(150, COPY_BACK),
|
|
1753
|
+
...swapLabels(0, COPY_BACK),
|
|
1754
|
+
{ id: "copy", step: 1, attrs: { transform: [copyPose(0.3), copyPose(1)], opacity: [0, 1] }, delay: COPY_BACK + 80, ms: 260, ease: "out" },
|
|
1755
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{success}}", "{{button}}"] }, delay: COPY_BACK, ms: 300 }
|
|
1756
|
+
]
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
var SEND_BACK = 1800;
|
|
1760
|
+
var planeScale = (s) => `scale(${r2(s)}) translate(-12 -12)`;
|
|
1761
|
+
var sendButton = {
|
|
1762
|
+
kind: "vector",
|
|
1763
|
+
id: "send-button",
|
|
1764
|
+
name: "Send button",
|
|
1765
|
+
description: "A send button whose paper plane winds up and flies off up and to the right as the label turns to \u201CSent\u201D with a check, then it resets.",
|
|
1766
|
+
category: "interaction",
|
|
1767
|
+
tags: ["interactive", "ui", "click", "send", "submit", "message", "button", "micro-interaction"],
|
|
1768
|
+
trigger: "click",
|
|
1769
|
+
hold: 600,
|
|
1770
|
+
presets: [
|
|
1771
|
+
{ name: "Sky", values: { button: [0.05, 0.6, 0.9], success: [0.05, 0.45, 0.7] } },
|
|
1772
|
+
{ name: "Mail", values: { label: "Send email", doneText: "Delivered", button: [0.9, 0.3, 0.25], success: [0.13, 0.6, 0.35] } }
|
|
1773
|
+
],
|
|
1774
|
+
derived: (v) => buttonLabels(v.label, v.doneText, 120, 20),
|
|
1775
|
+
motion: {
|
|
1776
|
+
svg: `<svg viewBox="0 0 240 100"><!-- {{label: text default='Send' label='Label'}} {{doneText: text default='Sent' label='Sent label'}} {{background: color default=#FFFFFF label='Background'}} {{button: color default=#4F46E5 label='Button'}} {{success: color default=#16A34A label='Sent'}} {{ink: color default=#FFFFFF label='Text and icon'}} --><rect width="240" height="100" fill="{{background}}"/><rect id="btn" x="20" y="24" width="200" height="52" rx="26" fill="{{button}}"/><path id="flight" d="M56 50 C88 46 150 22 262 -36" fill="none"/><g transform="translate(56 50) scale(1.1) translate(-12 -12)"><path id="check" d="M5 12.5 L10 17.5 L19 7" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g>${labelTexts(82)}<g id="plane" transform="translate(56 50)"><g id="planeIcon" transform="${planeScale(1.05)}" opacity="1"><path d="M22 2 L15 22 L11 13 L2 9 Z" fill="{{ink}}" stroke="{{ink}}" stroke-width="1.5" stroke-linejoin="round"/><path d="M22 2 L11 13" stroke="{{button}}" stroke-width="1.5" stroke-linecap="round"/></g></g></svg>`,
|
|
1777
|
+
animate: [
|
|
1778
|
+
// Wind up, then fly off along the curve, shrinking and fading.
|
|
1779
|
+
{ id: "planeIcon", step: 1, attrs: { transform: [planeScale(1.05), planeScale(0.88)] }, ms: 120, ease: "out" },
|
|
1780
|
+
{ id: "plane", step: 1, along: "flight", delay: 120, ms: 600, ease: "in-out" },
|
|
1781
|
+
{ id: "planeIcon", step: 1, attrs: { transform: [planeScale(0.88), planeScale(0.5)] }, delay: 120, ms: 600, ease: "in-out" },
|
|
1782
|
+
{ id: "planeIcon", step: 1, attrs: { opacity: [1, 0] }, delay: 520, ms: 200 },
|
|
1783
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{button}}", "{{success}}"] }, delay: 450, ms: 250 },
|
|
1784
|
+
...swapLabels(150, SEND_BACK),
|
|
1785
|
+
...checkFlash(500, SEND_BACK),
|
|
1786
|
+
// Back home: the plane pops in where it started.
|
|
1787
|
+
{ id: "plane", step: 1, along: "flight", at: [0, 0], delay: SEND_BACK, ms: 0 },
|
|
1788
|
+
{ id: "planeIcon", step: 1, attrs: { transform: [planeScale(0), planeScale(1.05)], opacity: [0, 1] }, delay: SEND_BACK + 100, ms: 300, ease: "out" },
|
|
1789
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{success}}", "{{button}}"] }, delay: SEND_BACK, ms: 300 }
|
|
1790
|
+
]
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
var [ARROW, RING] = morphPaths(
|
|
1794
|
+
"M10.6 4 H13.4 V11.6 H17.2 L12 17 L6.8 11.6 H10.6 Z M5 18.6 H19 V20.6 H5 Z",
|
|
1795
|
+
(() => {
|
|
1796
|
+
const circle2 = (r) => `M${Array.from({ length: 48 }, (_, k) => {
|
|
1797
|
+
const a = k / 48 * Math.PI * 2 - Math.PI / 2;
|
|
1798
|
+
return `${r2(12 + r * Math.cos(a))} ${r2(12 + r * Math.sin(a))}`;
|
|
1799
|
+
}).join(" L")} Z`;
|
|
1800
|
+
return `${circle2(9.6)} ${circle2(8)}`;
|
|
1801
|
+
})(),
|
|
1802
|
+
{ points: 56 }
|
|
1803
|
+
);
|
|
1804
|
+
var RING_R = r2(8.8 * 2.2);
|
|
1805
|
+
var DOWNLOAD_BACK = 2300;
|
|
1806
|
+
var iconAt = (y) => `translate(60 ${y}) scale(2.2) translate(-12 -12)`;
|
|
1807
|
+
var downloadButton = {
|
|
1808
|
+
kind: "vector",
|
|
1809
|
+
id: "download-button",
|
|
1810
|
+
name: "Download button",
|
|
1811
|
+
description: "A round download button whose arrow drops and morphs into a progress ring that fills, ends on a check, then resets.",
|
|
1812
|
+
category: "interaction",
|
|
1813
|
+
tags: ["interactive", "ui", "click", "download", "progress", "morph", "button", "micro-interaction"],
|
|
1814
|
+
trigger: "click",
|
|
1815
|
+
hold: 600,
|
|
1816
|
+
presets: [
|
|
1817
|
+
{ name: "Light", values: { background: [1, 1, 1], button: [0.93, 0.95, 1], ink: [0.31, 0.27, 0.9], track: [0.78, 0.8, 0.95], progress: [0.31, 0.27, 0.9], success: [0.86, 0.97, 0.9] } },
|
|
1818
|
+
{ name: "Ocean", values: { button: [0.03, 0.4, 0.6], progress: [0.4, 0.95, 0.85], success: [0.05, 0.55, 0.5] } }
|
|
1819
|
+
],
|
|
1820
|
+
motion: {
|
|
1821
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{background: color default=#0F172A label='Background'}} {{button: color default=#4F46E5 label='Button'}} {{success: color default=#16A34A label='Done'}} {{ink: color default=#FFFFFF label='Icon'}} {{track: color default=#818CF8 label='Ring track'}} {{progress: color default=#FFFFFF label='Progress'}} --><rect width="120" height="120" fill="{{background}}"/><circle id="btn" cx="60" cy="60" r="42" fill="{{button}}"/><g id="icon" transform="${iconAt(60)}"><path id="glyph" d="${ARROW}" fill="{{ink}}" fill-rule="evenodd"/></g><circle id="bar" cx="60" cy="60" r="${RING_R}" fill="none" stroke="{{progress}}" stroke-width="3.6" transform="rotate(-90 60 60)"/><g transform="translate(60 60) scale(1.5) translate(-12 -12)"><path id="check" d="M6 12.5 L10.5 17 L18.5 8" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g></svg>`,
|
|
1822
|
+
animate: [
|
|
1823
|
+
// The arrow drops, then rises back as it morphs into the ring's track.
|
|
1824
|
+
{ id: "icon", step: 1, attrs: { transform: [iconAt(60), iconAt(67)] }, ms: 180, ease: "out" },
|
|
1825
|
+
{ id: "icon", step: 1, attrs: { transform: [iconAt(67), iconAt(60)] }, delay: 180, ms: 260, ease: "in-out" },
|
|
1826
|
+
{ id: "glyph", step: 1, attrs: { d: [ARROW, RING], fill: ["{{ink}}", "{{track}}"] }, delay: 180, ms: 340, ease: "in-out" },
|
|
1827
|
+
// Progress round the ring (a flat-ended line, so nothing shows at 0), then a check.
|
|
1828
|
+
{ id: "bar", step: 1, draw: true, delay: 520, ms: 900, ease: "in-out" },
|
|
1829
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{button}}", "{{success}}"] }, delay: 1400, ms: 250 },
|
|
1830
|
+
{ id: "check", step: 1, attrs: { opacity: [0, 1] }, delay: 1450, ms: 40 },
|
|
1831
|
+
{ id: "check", step: 1, draw: true, delay: 1450, ms: 300, ease: "out" },
|
|
1832
|
+
// Reset: check and ring fade, the track morphs back into the arrow.
|
|
1833
|
+
{ id: "check", step: 1, attrs: { opacity: [1, 0] }, delay: DOWNLOAD_BACK, ms: 150 },
|
|
1834
|
+
{ id: "bar", step: 1, attrs: { opacity: [1, 0] }, delay: DOWNLOAD_BACK, ms: 200 },
|
|
1835
|
+
{ id: "check", step: 1, draw: [0, 0], delay: DOWNLOAD_BACK + 200, ms: 0 },
|
|
1836
|
+
{ id: "bar", step: 1, draw: [0, 0], delay: DOWNLOAD_BACK + 200, ms: 0 },
|
|
1837
|
+
snap("bar", "opacity", 1, DOWNLOAD_BACK + 200),
|
|
1838
|
+
{ id: "glyph", step: 1, attrs: { d: [RING, ARROW], fill: ["{{track}}", "{{ink}}"] }, delay: DOWNLOAD_BACK + 100, ms: 420, ease: "in-out" },
|
|
1839
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{success}}", "{{button}}"] }, delay: DOWNLOAD_BACK, ms: 300 }
|
|
1840
|
+
]
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
var linkHover = {
|
|
1844
|
+
kind: "vector",
|
|
1845
|
+
id: "link-hover",
|
|
1846
|
+
name: "Link hover",
|
|
1847
|
+
description: "A text link whose underline grows from left to right on hover, its arrow nudging right as the text changes colour.",
|
|
1848
|
+
category: "interaction",
|
|
1849
|
+
tags: ["interactive", "ui", "hover", "link", "underline", "navigation", "micro-interaction"],
|
|
1850
|
+
trigger: "hover",
|
|
1851
|
+
hold: 800,
|
|
1852
|
+
presets: [
|
|
1853
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], ink: [0.85, 0.87, 0.92], hover: [0.4, 0.85, 1] } },
|
|
1854
|
+
{ name: "Editorial", values: { text: "Continue reading", ink: [0.1, 0.1, 0.1], hover: [0.85, 0.2, 0.15] } }
|
|
1855
|
+
],
|
|
1856
|
+
// The text and its arrow centred together; the arrow is drawn round (0, 0) and placed.
|
|
1857
|
+
derived: (v) => {
|
|
1858
|
+
const { size, length } = fit(v.text, 230, 28);
|
|
1859
|
+
const total = length + size * 1.05;
|
|
1860
|
+
const left = 160 - total / 2;
|
|
1861
|
+
const base = 50 + size * 0.35;
|
|
1862
|
+
const ax = left + length + size * 0.35;
|
|
1863
|
+
const ay = base - size * 0.33;
|
|
1864
|
+
const s = size / 28;
|
|
1865
|
+
return {
|
|
1866
|
+
size,
|
|
1867
|
+
length,
|
|
1868
|
+
left: r1(left),
|
|
1869
|
+
base: r1(base),
|
|
1870
|
+
lineY: r1(base + size * 0.2),
|
|
1871
|
+
lineH: r1(Math.max(2, size * 0.08)),
|
|
1872
|
+
arrowAt: `translate(${r1(ax)} ${r1(ay)}) scale(${r2(s)})`,
|
|
1873
|
+
arrowTo: `translate(${r1(ax + size * 0.25)} ${r1(ay)}) scale(${r2(s)})`
|
|
1874
|
+
};
|
|
1875
|
+
},
|
|
1876
|
+
motion: {
|
|
1877
|
+
svg: `<svg viewBox="0 0 320 100"><!-- {{text: text default='Read the docs' label='Link text'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#1F2937 label='Text colour'}} {{hover: color default=#4F46E5 label='Hover colour'}} --><rect width="320" height="100" fill="{{background}}"/><text id="link" x="{{left}}" y="{{base}}" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{text}}</text><rect id="under" x="{{left}}" y="{{lineY}}" width="0" height="{{lineH}}" rx="1" fill="{{hover}}"/><g id="arrow" transform="{{arrowAt}}"><path id="arrowPath" d="M0 0 H18 M11 -7 L18 0 L11 7" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></g></svg>`,
|
|
1878
|
+
animate: [
|
|
1879
|
+
{ id: "link", attrs: { fill: ["{{ink}}", "{{hover}}"] }, ms: 220 },
|
|
1880
|
+
{ id: "under", attrs: { width: [0, n("length")] }, ms: 320, ease: "out" },
|
|
1881
|
+
{ id: "arrow", attrs: { transform: ["{{arrowAt}}", "{{arrowTo}}"] }, ms: 300, ease: "out" },
|
|
1882
|
+
{ id: "arrowPath", attrs: { stroke: ["{{ink}}", "{{hover}}"] }, ms: 220 }
|
|
1883
|
+
]
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
var buttonHoverFill = {
|
|
1887
|
+
kind: "vector",
|
|
1888
|
+
id: "button-hover-fill",
|
|
1889
|
+
name: "Button hover fill",
|
|
1890
|
+
description: "An outlined pill button that fills with a slanted sweep from the left on hover, its label inverting colour as the fill passes.",
|
|
1891
|
+
category: "interaction",
|
|
1892
|
+
tags: ["interactive", "ui", "hover", "button", "fill", "cta", "micro-interaction"],
|
|
1893
|
+
trigger: "hover",
|
|
1894
|
+
hold: 800,
|
|
1895
|
+
presets: [
|
|
1896
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], surface: [0.06, 0.07, 0.1], accent: [0.98, 0.8, 0.2] } },
|
|
1897
|
+
{ name: "Coral", values: { label: "Book a demo", accent: [0.96, 0.35, 0.3] } }
|
|
1898
|
+
],
|
|
1899
|
+
derived: (v) => {
|
|
1900
|
+
const { size, length } = fit(v.label, 190, 22);
|
|
1901
|
+
return { size, length, base: r1(60 + size * 0.35) };
|
|
1902
|
+
},
|
|
1903
|
+
motion: (() => {
|
|
1904
|
+
const sweep = (x) => `M10 26 H${x} L${x - 24} 94 H10 Z`;
|
|
1905
|
+
const label = (fill, clip = "") => `<text x="160" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="700" letter-spacing="0.5" fill="${fill}"${clip}>{{label}}</text>`;
|
|
1906
|
+
return {
|
|
1907
|
+
svg: `<svg viewBox="0 0 320 120"><!-- {{label: text default='Get started' label='Label'}} {{background: color default=#FFFFFF label='Background'}} {{surface: color default=#FFFFFF label='Button'}} {{accent: color default=#4F46E5 label='Accent'}} --><defs><clipPath id="swept"><path id="cover" d="${sweep(40)}"/></clipPath></defs><rect width="320" height="120" fill="{{background}}"/><g id="btn" transform="translate(0 0)"><rect x="40" y="30" width="240" height="60" rx="30" fill="{{surface}}" stroke="{{accent}}" stroke-width="2.5"/><rect x="40" y="30" width="240" height="60" rx="30" fill="{{accent}}" stroke="{{accent}}" stroke-width="2.5" clip-path="url(#swept)"/>${label("{{accent}}")}${label("{{surface}}", ` clip-path="url(#swept)"`)}</g></svg>`,
|
|
1908
|
+
animate: [
|
|
1909
|
+
{ id: "cover", attrs: { d: [sweep(40), sweep(310)] }, ms: 380, ease: "in-out" },
|
|
1910
|
+
{ id: "btn", attrs: { transform: ["translate(0 0)", "translate(0 -2)"] }, ms: 240, ease: "out" }
|
|
1911
|
+
]
|
|
1912
|
+
};
|
|
1913
|
+
})()
|
|
1914
|
+
};
|
|
1915
|
+
var tipPose = (s, dy) => `translate(160 ${104 + dy}) scale(${r2(s)}) translate(-160 -104)`;
|
|
1916
|
+
var tooltipPop = {
|
|
1917
|
+
kind: "vector",
|
|
1918
|
+
id: "tooltip-pop",
|
|
1919
|
+
name: "Tooltip pop",
|
|
1920
|
+
description: "A small help badge that pops a tooltip bubble up above it on hover, rising and fading in with a slight overshoot.",
|
|
1921
|
+
category: "interaction",
|
|
1922
|
+
tags: ["interactive", "ui", "hover", "tooltip", "help", "popover", "micro-interaction"],
|
|
1923
|
+
trigger: "hover",
|
|
1924
|
+
hold: 800,
|
|
1925
|
+
presets: [
|
|
1926
|
+
{ name: "Light", values: { background: [0.97, 0.97, 0.98], bubble: [1, 1, 1], tipInk: [0.1, 0.1, 0.15], badge: [0.6, 0.63, 0.7] } },
|
|
1927
|
+
{ name: "Warning", values: { tip: "This can't be undone", accent: [0.95, 0.4, 0.15], bubble: [0.95, 0.4, 0.15], tipInk: [1, 1, 1] } }
|
|
1928
|
+
],
|
|
1929
|
+
// The bubble is as wide as its text, centred over the badge.
|
|
1930
|
+
derived: (v) => {
|
|
1931
|
+
const { size, length } = fit(v.tip, 250, 15);
|
|
1932
|
+
const w = length + 28;
|
|
1933
|
+
return { size, length, bx: r1(160 - w / 2), bw: r1(w), tipY: r1(79 + size * 0.35) };
|
|
1934
|
+
},
|
|
1935
|
+
motion: {
|
|
1936
|
+
svg: `<svg viewBox="0 0 320 180"><!-- {{tip: text default='Saved to your library' label='Tooltip'}} {{background: color default=#0F172A label='Background'}} {{badge: color default=#475569 label='Badge'}} {{accent: color default=#6366F1 label='Badge hovered'}} {{bubble: color default=#F8FAFC label='Bubble'}} {{tipInk: color default=#0F172A label='Tooltip text'}} --><rect width="320" height="180" fill="{{background}}"/><g id="tip" transform="${tipPose(0.5, 8)}" opacity="0"><rect x="{{bx}}" y="60" width="{{bw}}" height="38" rx="10" fill="{{bubble}}"/><path d="M151 96 L160 105 L169 96 Z" fill="{{bubble}}"/><text x="160" y="{{tipY}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="500" fill="{{tipInk}}">{{tip}}</text></g><g id="badgeG" transform="${about(160, 134, 1)}"><circle id="badge" cx="160" cy="134" r="18" fill="{{badge}}"/><text x="160" y="141" text-anchor="middle" font-family="${SANS}" font-size="20" font-weight="700" fill="#FFFFFF">?</text></g></svg>`,
|
|
1937
|
+
animate: [
|
|
1938
|
+
{ id: "tip", attrs: { transform: [tipPose(0.5, 8), tipPose(1.07, -3)] }, ms: 190, ease: "out" },
|
|
1939
|
+
{ id: "tip", attrs: { transform: [tipPose(1.07, -3), tipPose(1, 0)] }, delay: 190, ms: 150, ease: "in-out" },
|
|
1940
|
+
{ id: "tip", attrs: { opacity: [0, 1] }, ms: 150 },
|
|
1941
|
+
{ id: "badge", attrs: { fill: ["{{badge}}", "{{accent}}"] }, ms: 200 },
|
|
1942
|
+
{ id: "badgeG", attrs: { transform: [about(160, 134, 1), about(160, 134, 1.08)] }, ms: 200, ease: "out" }
|
|
1943
|
+
]
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
var pressButton = {
|
|
1947
|
+
kind: "vector",
|
|
1948
|
+
id: "press-button",
|
|
1949
|
+
name: "Press button",
|
|
1950
|
+
description: "A chunky 3D button whose face pushes down onto its base while held, its shadow tightening, and springs back up on release.",
|
|
1951
|
+
category: "interaction",
|
|
1952
|
+
tags: ["interactive", "ui", "press", "button", "3d", "tactile", "micro-interaction"],
|
|
1953
|
+
trigger: "press",
|
|
1954
|
+
hold: 800,
|
|
1955
|
+
presets: [
|
|
1956
|
+
{ name: "Arcade red", values: { face: [0.94, 0.27, 0.27], background: [0.1, 0.09, 0.12] } },
|
|
1957
|
+
{ name: "Mint", values: { face: [0.3, 0.85, 0.6], ink: [0.05, 0.25, 0.15], label: "Let's go" } }
|
|
1958
|
+
],
|
|
1959
|
+
derived: (v) => {
|
|
1960
|
+
const { size, length } = fit(v.label, 170, 22);
|
|
1961
|
+
return { size, length, base: r1(67 + size * 0.35), side: mixHex(v.face, [0, 0, 0], 0.35), pressed: mixHex(v.face, [0, 0, 0], 0.08) };
|
|
1962
|
+
},
|
|
1963
|
+
// Played backwards on release, the brief lift at the start of the press becomes a spring past rest at the end.
|
|
1964
|
+
motion: {
|
|
1965
|
+
svg: `<svg viewBox="0 0 320 140"><!-- {{label: text default='Press me' label='Label'}} {{background: color default=#F1F5F9 label='Background'}} {{face: color default=#6366F1 label='Button'}} {{ink: color default=#FFFFFF label='Label colour'}} {{shadow: color default=#0F172A label='Shadow'}} --><rect width="320" height="140" fill="{{background}}"/><ellipse id="shadow" cx="160" cy="114" rx="108" ry="9" fill="{{shadow}}" opacity="0.3"/><rect x="60" y="50" width="200" height="58" rx="16" fill="{{side}}"/><g id="faceG" transform="translate(0 0)"><rect id="faceRect" x="60" y="38" width="200" height="58" rx="16" fill="{{face}}"/><text x="160" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="800" fill="{{ink}}">{{label}}</text></g></svg>`,
|
|
1966
|
+
animate: [
|
|
1967
|
+
{ id: "faceG", attrs: { transform: ["translate(0 0)", "translate(0 -1.5)"] }, ms: 40, ease: "out" },
|
|
1968
|
+
{ id: "faceG", attrs: { transform: ["translate(0 -1.5)", "translate(0 9)"] }, delay: 40, ms: 90, ease: "out" },
|
|
1969
|
+
{ id: "shadow", attrs: { rx: [108, 96], ry: [9, 5], opacity: [0.3, 0.18] }, delay: 40, ms: 90, ease: "out" },
|
|
1970
|
+
{ id: "faceRect", attrs: { fill: ["{{face}}", "{{pressed}}"] }, delay: 40, ms: 90 }
|
|
1971
|
+
]
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
var interactionEffects = [
|
|
1975
|
+
likeButton,
|
|
1976
|
+
toggleSwitch,
|
|
1977
|
+
checkbox,
|
|
1978
|
+
menuToggle,
|
|
1979
|
+
bookmark,
|
|
1980
|
+
copyButton,
|
|
1981
|
+
sendButton,
|
|
1982
|
+
downloadButton,
|
|
1983
|
+
linkHover,
|
|
1984
|
+
buttonHoverFill,
|
|
1985
|
+
tooltipPop,
|
|
1986
|
+
pressButton
|
|
1987
|
+
];
|
|
1988
|
+
|
|
1468
1989
|
// src/catalog/morph.ts
|
|
1469
1990
|
var NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
|
|
1470
1991
|
var DEG = Math.PI / 180;
|
|
1471
|
-
var
|
|
1992
|
+
var r22 = (v) => {
|
|
1472
1993
|
const r = Math.round(v * 100) / 100;
|
|
1473
1994
|
return Object.is(r, -0) ? 0 : r;
|
|
1474
1995
|
};
|
|
1475
|
-
var
|
|
1996
|
+
var n2 = (name) => `{{${name}}}`;
|
|
1476
1997
|
var pointsOf = (d) => {
|
|
1477
1998
|
const nums = (d.match(NUMBER) ?? []).map(Number);
|
|
1478
1999
|
return Array.from({ length: nums.length >> 1 }, (_, i) => [nums[2 * i], nums[2 * i + 1]]);
|
|
@@ -1482,10 +2003,10 @@ function mapPath(d, f) {
|
|
|
1482
2003
|
let i = 0;
|
|
1483
2004
|
return d.replace(NUMBER, () => {
|
|
1484
2005
|
const k = i++;
|
|
1485
|
-
return String(
|
|
2006
|
+
return String(r22(moved[k >> 1][k % 2]));
|
|
1486
2007
|
});
|
|
1487
2008
|
}
|
|
1488
|
-
var polyline = (pts, close = false) => `M${pts.map((p) => `${
|
|
2009
|
+
var polyline = (pts, close = false) => `M${pts.map((p) => `${r22(p[0])} ${r22(p[1])}`).join(" L")}${close ? " Z" : ""}`;
|
|
1489
2010
|
function arc(cx, cy, rx, ry, from, to, every = 8) {
|
|
1490
2011
|
const steps = Math.max(1, Math.ceil(Math.abs(to - from) / every));
|
|
1491
2012
|
return Array.from({ length: steps + 1 }, (_, k) => {
|
|
@@ -1532,20 +2053,20 @@ function around(c, r, from, to, avoid) {
|
|
|
1532
2053
|
if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
|
|
1533
2054
|
return arc(c[0], c[1], r, r, from, from + delta);
|
|
1534
2055
|
}
|
|
1535
|
-
function crescent(c1,
|
|
2056
|
+
function crescent(c1, r13, c2, r2_) {
|
|
1536
2057
|
const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
|
|
1537
2058
|
const d = Math.hypot(dx, dy);
|
|
1538
|
-
const a = (
|
|
1539
|
-
const h = Math.sqrt(Math.max(0,
|
|
2059
|
+
const a = (r13 * r13 - r2_ * r2_ + d * d) / (2 * d);
|
|
2060
|
+
const h = Math.sqrt(Math.max(0, r13 * r13 - a * a));
|
|
1540
2061
|
const [px, py] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
|
|
1541
2062
|
const i1 = [px - h * dy / d, py + h * dx / d];
|
|
1542
2063
|
const i2 = [px + h * dy / d, py - h * dx / d];
|
|
1543
2064
|
const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG;
|
|
1544
|
-
const outer = around(c1,
|
|
2065
|
+
const outer = around(c1, r13, angle(c1, i1), angle(c1, i2), angle(c1, c2));
|
|
1545
2066
|
const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
|
|
1546
2067
|
return polyline([...outer, ...inner.slice(1, -1)], true);
|
|
1547
2068
|
}
|
|
1548
|
-
var
|
|
2069
|
+
var HEART3 = "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
2070
|
var placed = (d, cx, cy, s) => mapPath(d, (p) => [cx + (p[0] - 12) * s, cy + (p[1] - 12) * s]);
|
|
1550
2071
|
function fitted(d, box, points) {
|
|
1551
2072
|
const outline = morphSequence([d], { points })[0];
|
|
@@ -1596,8 +2117,8 @@ var shapeMorph = {
|
|
|
1596
2117
|
motion: {
|
|
1597
2118
|
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
2119
|
animate: [
|
|
1599
|
-
{ id: "shape", attrs: { d: ["{{a}}", "{{b}}"], fill: ["{{fillFrom}}", "{{fillTo}}"], stroke: ["{{strokeFrom}}", "{{strokeTo}}"] }, delay:
|
|
1600
|
-
{ id: "shape", attrs: { d: ["{{b}}", "{{a}}"], fill: ["{{fillTo}}", "{{fillFrom}}"], stroke: ["{{strokeTo}}", "{{strokeFrom}}"] }, delay:
|
|
2120
|
+
{ id: "shape", attrs: { d: ["{{a}}", "{{b}}"], fill: ["{{fillFrom}}", "{{fillTo}}"], stroke: ["{{strokeFrom}}", "{{strokeTo}}"] }, delay: n2("pause"), ms: n2("morph"), ease: "in-out" },
|
|
2121
|
+
{ id: "shape", attrs: { d: ["{{b}}", "{{a}}"], fill: ["{{fillTo}}", "{{fillFrom}}"], stroke: ["{{strokeTo}}", "{{strokeFrom}}"] }, delay: n2("back"), ms: n2("morph"), ease: "in-out" }
|
|
1601
2122
|
]
|
|
1602
2123
|
}
|
|
1603
2124
|
};
|
|
@@ -1624,7 +2145,7 @@ var ICON_PAIRS = (() => {
|
|
|
1624
2145
|
],
|
|
1625
2146
|
["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
2147
|
[sun.join(" "), crescent([11.5, 12.5], 8.5, [16.5, 7.5], 7)],
|
|
1627
|
-
[`${
|
|
2148
|
+
[`${HEART3} ${placed(HEART3, 12, 12, 0.7)}`, HEART3]
|
|
1628
2149
|
];
|
|
1629
2150
|
})();
|
|
1630
2151
|
var iconMorphs = /* @__PURE__ */ new Map();
|
|
@@ -1665,7 +2186,7 @@ var iconToggle = {
|
|
|
1665
2186
|
}
|
|
1666
2187
|
};
|
|
1667
2188
|
var SEQUENCE = morphSequence(
|
|
1668
|
-
[circle(100, 100, 72), star(100, 104, 5, 82, 36), placed(
|
|
2189
|
+
[circle(100, 100, 72), star(100, 104, 5, 82, 36), placed(HEART3, 100, 98, 8.4), star(100, 100, 3, 78, 78)],
|
|
1669
2190
|
{ points: 96 }
|
|
1670
2191
|
);
|
|
1671
2192
|
var morphSequenceEffect = {
|
|
@@ -1688,7 +2209,7 @@ var morphSequenceEffect = {
|
|
|
1688
2209
|
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
2210
|
animate: SEQUENCE.map((d, i) => {
|
|
1690
2211
|
const next = (i + 1) % SEQUENCE.length;
|
|
1691
|
-
return { id: "shape", attrs: { d: [d, SEQUENCE[next]], fill: [`{{c${i}}}`, `{{c${next}}}`] }, delay:
|
|
2212
|
+
return { id: "shape", attrs: { d: [d, SEQUENCE[next]], fill: [`{{c${i}}}`, `{{c${next}}}`] }, delay: n2(`at${i}`), ms: n2("morph"), ease: "in-out" };
|
|
1692
2213
|
})
|
|
1693
2214
|
}
|
|
1694
2215
|
};
|
|
@@ -1729,13 +2250,13 @@ var gooeyMorph = {
|
|
|
1729
2250
|
motion: {
|
|
1730
2251
|
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
2252
|
animate: GOO.flatMap(
|
|
1732
|
-
(stages, i) => stages.map((d, s) => ({ id: `goo${i}`, attrs: { d: [d, stages[(s + 1) % stages.length]] }, delay:
|
|
2253
|
+
(stages, i) => stages.map((d, s) => ({ id: `goo${i}`, attrs: { d: [d, stages[(s + 1) % stages.length]] }, delay: n2(`at${s}`), ms: n2("morph"), ease: "in-out" }))
|
|
1733
2254
|
)
|
|
1734
2255
|
}
|
|
1735
2256
|
};
|
|
1736
2257
|
var DOTS = 48;
|
|
1737
2258
|
var DOT_STAGGER = 14;
|
|
1738
|
-
var PARTICLE_FROM =
|
|
2259
|
+
var PARTICLE_FROM = HEART3;
|
|
1739
2260
|
var PARTICLE_TO = star(12, 12, 5, 10, 4.2);
|
|
1740
2261
|
var noise = (i) => {
|
|
1741
2262
|
const x = Math.sin(i * 12.9898 + 4.1414) * 43758.5453;
|
|
@@ -1764,15 +2285,15 @@ var particleMorph = {
|
|
|
1764
2285
|
for (let i = 0; i < DOTS; i++) {
|
|
1765
2286
|
const k = Math.floor(i * pa.length / DOTS);
|
|
1766
2287
|
const [p, q] = [pa[k], pb[k]];
|
|
1767
|
-
out[`ax${i}`] =
|
|
1768
|
-
out[`ay${i}`] =
|
|
1769
|
-
out[`bx${i}`] =
|
|
1770
|
-
out[`by${i}`] =
|
|
2288
|
+
out[`ax${i}`] = r22(p[0]);
|
|
2289
|
+
out[`ay${i}`] = r22(p[1]);
|
|
2290
|
+
out[`bx${i}`] = r22(q[0]);
|
|
2291
|
+
out[`by${i}`] = r22(q[1]);
|
|
1771
2292
|
for (const [name, seed] of [["s", i], ["t", i + DOTS]]) {
|
|
1772
2293
|
const angle = seed * 2.39996;
|
|
1773
2294
|
const far = scatter * (0.45 + 0.55 * noise(seed));
|
|
1774
|
-
out[`${name}x${i}`] =
|
|
1775
|
-
out[`${name}y${i}`] =
|
|
2295
|
+
out[`${name}x${i}`] = r22((p[0] + q[0]) / 2 + far * Math.cos(angle));
|
|
2296
|
+
out[`${name}y${i}`] = r22((p[1] + q[1]) / 2 + far * Math.sin(angle));
|
|
1776
2297
|
}
|
|
1777
2298
|
}
|
|
1778
2299
|
return out;
|
|
@@ -1781,10 +2302,10 @@ var particleMorph = {
|
|
|
1781
2302
|
const back = 3e3;
|
|
1782
2303
|
const spread = DOTS * DOT_STAGGER;
|
|
1783
2304
|
const leg = (i, at, from, mid, to, colours) => [
|
|
1784
|
-
{ id: `p${i}`, attrs: { cx: [
|
|
1785
|
-
{ id: `p${i}`, attrs: { cx: [
|
|
2305
|
+
{ id: `p${i}`, attrs: { cx: [n2(`${from}x${i}`), n2(`${mid}x${i}`)], cy: [n2(`${from}y${i}`), n2(`${mid}y${i}`)] }, delay: at + i * DOT_STAGGER, ms: 450, ease: "out" },
|
|
2306
|
+
{ id: `p${i}`, attrs: { cx: [n2(`${mid}x${i}`), n2(`${to}x${i}`)], cy: [n2(`${mid}y${i}`), n2(`${to}y${i}`)], fill: colours }, delay: at + 450 + i * DOT_STAGGER, ms: 650, ease: "in-out" }
|
|
1786
2307
|
];
|
|
1787
|
-
const fade = (id, at, show) => ({ id, attrs: { opacity: show ? [0,
|
|
2308
|
+
const fade = (id, at, show) => ({ id, attrs: { opacity: show ? [0, n2("ghost")] : [n2("ghost"), 0] }, delay: at, ms: 450, ease: "in-out" });
|
|
1788
2309
|
return {
|
|
1789
2310
|
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
2311
|
{ length: DOTS },
|
|
@@ -1845,11 +2366,11 @@ var digitMorph = {
|
|
|
1845
2366
|
const left = 200 - (used * 22 - 2) * scale / 2;
|
|
1846
2367
|
const interval = Number(v.morph) + DIGIT_REST;
|
|
1847
2368
|
const lastEnd = Math.max(0, steps - 1) * interval + Number(v.morph);
|
|
1848
|
-
const out = { scale:
|
|
2369
|
+
const out = { scale: r22(scale), y: r22(100 - 16 * scale), back: lastEnd + DIGIT_REST * 3 };
|
|
1849
2370
|
for (let k = 1; k <= MAX_STEPS; k++) out[`t${k}`] = k <= steps ? (k - 1) * interval : lastEnd;
|
|
1850
2371
|
for (let j = 0; j < SLOTS; j++) {
|
|
1851
2372
|
const place = j - (SLOTS - used);
|
|
1852
|
-
out[`x${j}`] =
|
|
2373
|
+
out[`x${j}`] = r22(left + Math.max(0, place) * 22 * scale);
|
|
1853
2374
|
const glyphs = values.map((x) => {
|
|
1854
2375
|
const text = String(x);
|
|
1855
2376
|
const at = text.length - (used - place);
|
|
@@ -1872,15 +2393,575 @@ var digitMorph = {
|
|
|
1872
2393
|
animate: Array.from({ length: SLOTS }, (_, j) => [
|
|
1873
2394
|
...Array.from({ length: MAX_STEPS }, (_2, i) => {
|
|
1874
2395
|
const k = i + 1;
|
|
1875
|
-
return { id: `s${j}`, attrs: { d: [`{{d${j}_${k - 1}}}`, `{{d${j}_${k}}}`], opacity: [
|
|
2396
|
+
return { id: `s${j}`, attrs: { d: [`{{d${j}_${k - 1}}}`, `{{d${j}_${k}}}`], opacity: [n2(`o${j}_${k - 1}`), n2(`o${j}_${k}`)] }, delay: n2(`t${k}`), ms: n2("morph"), ease: "in-out" };
|
|
1876
2397
|
}),
|
|
1877
2398
|
// 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: [
|
|
2399
|
+
{ id: `s${j}`, attrs: { d: [`{{d${j}_${MAX_STEPS}}}`, `{{d${j}_0}}`], opacity: [n2(`o${j}_${MAX_STEPS}`), n2(`o${j}_0`)] }, delay: n2("back"), ms: n2("morph"), ease: "in-out" }
|
|
1879
2400
|
]).flat()
|
|
1880
2401
|
}
|
|
1881
2402
|
};
|
|
1882
2403
|
var morphEffects = [shapeMorph, iconToggle, morphSequenceEffect, gooeyMorph, particleMorph, digitMorph];
|
|
1883
2404
|
|
|
2405
|
+
// src/catalog/shaders/interaction.ts
|
|
2406
|
+
var CLICKS = `
|
|
2407
|
+
vec4 clickAt(int i, bool demo) {
|
|
2408
|
+
if (!demo || iClicks[0].w > 0.5) return iClicks[i];
|
|
2409
|
+
float k = floor(iTime / 2.0) - float(i);
|
|
2410
|
+
vec2 h = hash22(vec2(k * 1.37 + 0.5, 3.7));
|
|
2411
|
+
return vec4((0.15 + 0.7 * h) * iResolution.xy, k * 2.0, 1.0);
|
|
2412
|
+
}`.trim();
|
|
2413
|
+
var HOVER = `
|
|
2414
|
+
vec3 hoverPoint(bool demo, float idle) {
|
|
2415
|
+
vec2 drift = iResolution.xy * (0.5 + vec2(0.28 * sin(iTime * 0.63), 0.22 * sin(iTime * 0.91 + 1.3)));
|
|
2416
|
+
float h = clamp(iPointer.z, 0.0, 1.0);
|
|
2417
|
+
return vec3(mix(drift, iPointer.xy, h), mix(demo ? idle : 0.0, 1.0, h));
|
|
2418
|
+
}`.trim();
|
|
2419
|
+
var GLOW = `
|
|
2420
|
+
vec4 glow(vec3 c) {
|
|
2421
|
+
c = 1.0 - exp(-max(c, 0.0));
|
|
2422
|
+
float a = max(c.r, max(c.g, c.b));
|
|
2423
|
+
return vec4(c / max(a, 1e-4), a);
|
|
2424
|
+
}`.trim();
|
|
2425
|
+
var OVER = `
|
|
2426
|
+
vec4 over(vec4 acc, vec3 c, float a) {
|
|
2427
|
+
return vec4(acc.rgb * (1.0 - a) + c * a, acc.a * (1.0 - a) + a);
|
|
2428
|
+
}
|
|
2429
|
+
vec4 unpremultiply(vec4 acc) {
|
|
2430
|
+
return vec4(acc.rgb / max(acc.a, 1e-4), acc.a);
|
|
2431
|
+
}`.trim();
|
|
2432
|
+
var clickRipple = {
|
|
2433
|
+
kind: "shader",
|
|
2434
|
+
id: "click-ripple",
|
|
2435
|
+
name: "Click ripple",
|
|
2436
|
+
description: "Rings of water spreading out from every click and fading as they go.",
|
|
2437
|
+
category: "interaction",
|
|
2438
|
+
tags: ["interactive", "click", "overlay", "water", "ripple"],
|
|
2439
|
+
transparent: true,
|
|
2440
|
+
source: glsl(
|
|
2441
|
+
`// Click ripple: water rings spreading from each click (up to 8 at once).
|
|
2442
|
+
uniform vec3 uColor; // @control color default=#BFE6FF label="Colour"
|
|
2443
|
+
uniform float uSpeed; // @control min=0.05 max=2 default=0.5 label="Speed"
|
|
2444
|
+
uniform float uWidth; // @control min=0.002 max=0.05 default=0.012 label="Ring width"
|
|
2445
|
+
uniform float uLife; // @control min=0.3 max=6 default=2 label="Lifetime"
|
|
2446
|
+
uniform int uRings; // @control min=1 max=4 default=3 label="Rings per click"
|
|
2447
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2448
|
+
HASH,
|
|
2449
|
+
CLICKS,
|
|
2450
|
+
OVER,
|
|
2451
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2452
|
+
float px = 1.0 / iResolution.y;
|
|
2453
|
+
vec4 acc = vec4(0.0);
|
|
2454
|
+
for (int i = 7; i >= 0; i--) {
|
|
2455
|
+
vec4 c = clickAt(i, uDemo);
|
|
2456
|
+
if (c.w < 0.5) continue;
|
|
2457
|
+
float age = iTime - c.z;
|
|
2458
|
+
if (age < 0.0 || age > uLife) continue;
|
|
2459
|
+
float life = age / uLife;
|
|
2460
|
+
float fade = (1.0 - life) * (1.0 - life);
|
|
2461
|
+
float d = length(fragCoord - c.xy) * px;
|
|
2462
|
+
for (int j = 0; j < 4; j++) {
|
|
2463
|
+
if (j >= uRings) break;
|
|
2464
|
+
float r = (age - float(j) * 0.16) * uSpeed;
|
|
2465
|
+
if (r <= 0.0) continue;
|
|
2466
|
+
// Rings widen and soften as they spread.
|
|
2467
|
+
float w = max(uWidth * (1.0 + life * 1.5), 1.5 * px);
|
|
2468
|
+
float x = (d - r) / w;
|
|
2469
|
+
float k = fade * (1.0 - 0.28 * float(j)) * smoothstep(0.0, 0.05, r);
|
|
2470
|
+
// A bright crest with a darker trough just inside it.
|
|
2471
|
+
float crest = exp(-x * x * 2.0);
|
|
2472
|
+
float trough = exp(-(x + 1.6) * (x + 1.6) * 2.0);
|
|
2473
|
+
acc = over(acc, uColor * 0.15, trough * k * 0.35);
|
|
2474
|
+
acc = over(acc, mix(uColor, vec3(1.0), 0.5 * crest), crest * k);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
fragColor = unpremultiply(acc);
|
|
2478
|
+
}`
|
|
2479
|
+
),
|
|
2480
|
+
presets: [
|
|
2481
|
+
{ name: "Sonar", values: { uColor: [0.3, 1, 0.6], uSpeed: 0.3, uWidth: 6e-3, uLife: 4, uRings: 4 } },
|
|
2482
|
+
{ name: "Raindrop", values: { uColor: [1, 1, 1], uSpeed: 0.8, uWidth: 0.02, uLife: 1.2, uRings: 2 } }
|
|
2483
|
+
]
|
|
2484
|
+
};
|
|
2485
|
+
var clickBurst = {
|
|
2486
|
+
kind: "shader",
|
|
2487
|
+
id: "click-burst",
|
|
2488
|
+
name: "Click burst",
|
|
2489
|
+
description: "Sparks or confetti bursting out of every click, falling under gravity as they fade.",
|
|
2490
|
+
category: "interaction",
|
|
2491
|
+
tags: ["interactive", "click", "overlay", "particles", "celebration", "confetti"],
|
|
2492
|
+
transparent: true,
|
|
2493
|
+
source: glsl(
|
|
2494
|
+
`// Click burst: sparks (or confetti) thrown out of each click, slowed by drag and pulled down by gravity.
|
|
2495
|
+
uniform vec3 uColorA; // @control color default=#FFD166 label="Colour 1"
|
|
2496
|
+
uniform vec3 uColorB; // @control color default=#EF476F label="Colour 2"
|
|
2497
|
+
uniform vec3 uColorC; // @control color default=#06D6A0 label="Colour 3"
|
|
2498
|
+
uniform int uCount; // @control min=4 max=48 default=24 label="Count"
|
|
2499
|
+
uniform float uSpread; // @control min=0.1 max=2 default=0.8 label="Spread"
|
|
2500
|
+
uniform float uGravity; // @control min=0 max=3 default=1 label="Gravity"
|
|
2501
|
+
uniform float uLife; // @control min=0.4 max=4 default=1.6 label="Lifetime"
|
|
2502
|
+
uniform bool uConfetti; // @control default=false label="Confetti"
|
|
2503
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2504
|
+
HASH,
|
|
2505
|
+
CLICKS,
|
|
2506
|
+
OVER,
|
|
2507
|
+
`float segment(vec2 p, vec2 a, vec2 b) {
|
|
2508
|
+
vec2 ab = b - a;
|
|
2509
|
+
float t = clamp(dot(p - a, ab) / max(dot(ab, ab), 1e-8), 0.0, 1.0);
|
|
2510
|
+
return length(p - a - ab * t);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2514
|
+
float px = 1.0 / iResolution.y;
|
|
2515
|
+
vec2 p = fragCoord * px;
|
|
2516
|
+
const float drag = 2.5;
|
|
2517
|
+
vec4 acc = vec4(0.0);
|
|
2518
|
+
for (int i = 7; i >= 0; i--) {
|
|
2519
|
+
vec4 c = clickAt(i, uDemo);
|
|
2520
|
+
if (c.w < 0.5) continue;
|
|
2521
|
+
float age = iTime - c.z;
|
|
2522
|
+
if (age < 0.0 || age > uLife) continue;
|
|
2523
|
+
float life = age / uLife;
|
|
2524
|
+
vec2 origin = c.xy * px;
|
|
2525
|
+
// A quick flash where it landed.
|
|
2526
|
+
float flash = exp(-age * 18.0) * exp(-dot(p - origin, p - origin) / 0.002);
|
|
2527
|
+
acc = over(acc, vec3(1.0), clamp(flash, 0.0, 1.0));
|
|
2528
|
+
float slow = (1.0 - exp(-drag * age)) / drag;
|
|
2529
|
+
for (int j = 0; j < 48; j++) {
|
|
2530
|
+
if (j >= uCount) break;
|
|
2531
|
+
vec2 h = hash22(vec2(float(j) * 7.13 + 1.0, c.z * 3.1 + float(i)));
|
|
2532
|
+
float h3 = hash12(h * 91.7 + c.z);
|
|
2533
|
+
float angle = h.x * 6.28318;
|
|
2534
|
+
vec2 v0 = vec2(cos(angle), sin(angle)) * uSpread * (0.3 + 0.7 * h.y) + vec2(0.0, uSpread * 0.35);
|
|
2535
|
+
vec2 pos = origin + v0 * slow - vec2(0.0, 0.5 * uGravity * 0.6 * age * age);
|
|
2536
|
+
if (abs(p.x - pos.x) > 0.06 || abs(p.y - pos.y) > 0.06) continue;
|
|
2537
|
+
vec3 col = h3 < 0.333 ? uColorA : (h3 < 0.667 ? uColorB : uColorC);
|
|
2538
|
+
float a;
|
|
2539
|
+
if (uConfetti) {
|
|
2540
|
+
// A tumbling paper rectangle.
|
|
2541
|
+
float spin = (h3 - 0.5) * 20.0 * age + angle;
|
|
2542
|
+
float cs = cos(spin);
|
|
2543
|
+
float sn = sin(spin);
|
|
2544
|
+
vec2 q = mat2(cs, sn, -sn, cs) * (p - pos);
|
|
2545
|
+
vec2 size = vec2(0.011, 0.006 * abs(cos(age * (4.0 + 6.0 * h.y) + h.x * 9.0)) + px);
|
|
2546
|
+
vec2 e = abs(q) - size;
|
|
2547
|
+
float sd = length(max(e, 0.0)) + min(max(e.x, e.y), 0.0);
|
|
2548
|
+
a = (1.0 - smoothstep(-px, px, sd)) * (1.0 - smoothstep(0.7, 1.0, life));
|
|
2549
|
+
col *= 0.75 + 0.25 * cs;
|
|
2550
|
+
} else {
|
|
2551
|
+
// A streak along its velocity, with a white-hot core.
|
|
2552
|
+
vec2 v = v0 * exp(-drag * age) - vec2(0.0, uGravity * 0.6 * age);
|
|
2553
|
+
float r = 0.004 * (1.0 - 0.6 * life) + px;
|
|
2554
|
+
float sd = segment(p, pos, pos - v * 0.05) - r;
|
|
2555
|
+
a = (1.0 - smoothstep(-px, px, sd)) * (1.0 - life);
|
|
2556
|
+
a += exp(-max(sd, 0.0) / (r * 2.0)) * 0.35 * (1.0 - life);
|
|
2557
|
+
col = mix(col, vec3(1.0), exp(-max(sd + r, 0.0) / (r * 0.5)) * 0.6);
|
|
2558
|
+
}
|
|
2559
|
+
acc = over(acc, col, clamp(a, 0.0, 1.0));
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
fragColor = unpremultiply(acc);
|
|
2563
|
+
}`
|
|
2564
|
+
),
|
|
2565
|
+
presets: [
|
|
2566
|
+
{ name: "Confetti", values: { uConfetti: true, uCount: 36, uSpread: 1, uGravity: 1.4, uLife: 2.5, uColorC: [0.3, 0.55, 1] } },
|
|
2567
|
+
{ name: "Embers", values: { uColorA: [1, 0.6, 0.15], uColorB: [1, 0.3, 0.1], uColorC: [1, 0.85, 0.5], uCount: 16, uSpread: 0.5, uGravity: 0.2 } }
|
|
2568
|
+
]
|
|
2569
|
+
};
|
|
2570
|
+
var cursorGlow = {
|
|
2571
|
+
kind: "shader",
|
|
2572
|
+
id: "cursor-glow",
|
|
2573
|
+
name: "Cursor glow",
|
|
2574
|
+
description: "A soft light following the pointer, with a halo ring that tightens while it is pressed.",
|
|
2575
|
+
category: "interaction",
|
|
2576
|
+
tags: ["interactive", "hover", "press", "overlay", "glow", "cursor"],
|
|
2577
|
+
transparent: true,
|
|
2578
|
+
source: glsl(
|
|
2579
|
+
`// Cursor glow: a soft light at the pointer, scaled by how hovered, with a halo that tightens while pressed.
|
|
2580
|
+
uniform vec3 uColor; // @control color default=#7DD3FC label="Colour"
|
|
2581
|
+
uniform float uRadius; // @control min=0.03 max=0.6 default=0.18 label="Radius"
|
|
2582
|
+
uniform float uIntensity; // @control min=0 max=3 default=1.2 label="Intensity"
|
|
2583
|
+
uniform float uHalo; // @control min=0 max=1 default=0.7 label="Halo"
|
|
2584
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2585
|
+
HOVER,
|
|
2586
|
+
GLOW,
|
|
2587
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2588
|
+
vec3 hp = hoverPoint(uDemo, 0.7);
|
|
2589
|
+
float d = length(fragCoord - hp.xy) / iResolution.y;
|
|
2590
|
+
// How pressed: eases in while held; the idle demo pulses it.
|
|
2591
|
+
float press = 1.0 - exp(-max(iPointer.w, 0.0) * 4.0);
|
|
2592
|
+
if (uDemo) press = max(press, (1.0 - iPointer.z) * pow(max(sin(iTime * 1.3), 0.0), 3.0));
|
|
2593
|
+
float light = exp(-pow(d / uRadius, 2.0) * 2.0) * uIntensity * hp.z * (1.0 + 0.6 * press);
|
|
2594
|
+
float ringR = uRadius * mix(1.3, 0.55, press);
|
|
2595
|
+
float ringW = max(uRadius * 0.05, 1.5 / iResolution.y);
|
|
2596
|
+
float ring = exp(-pow((d - ringR) / ringW, 2.0)) * uHalo * hp.z * (0.6 + 0.8 * press);
|
|
2597
|
+
fragColor = glow(uColor * light + mix(uColor, vec3(1.0), 0.5) * ring * 1.5);
|
|
2598
|
+
}`
|
|
2599
|
+
),
|
|
2600
|
+
presets: [
|
|
2601
|
+
{ name: "Spotlight", values: { uColor: [1, 0.95, 0.85], uRadius: 0.4, uIntensity: 0.8, uHalo: 0 } },
|
|
2602
|
+
{ name: "Neon", values: { uColor: [1, 0.2, 0.75], uRadius: 0.1, uIntensity: 2, uHalo: 1 } }
|
|
2603
|
+
]
|
|
2604
|
+
};
|
|
2605
|
+
var hoverLiquid = {
|
|
2606
|
+
kind: "shader",
|
|
2607
|
+
id: "hover-liquid",
|
|
2608
|
+
name: "Liquid hover",
|
|
2609
|
+
description: "An image that swells and ripples like liquid around the pointer while it hovers.",
|
|
2610
|
+
category: "interaction",
|
|
2611
|
+
tags: ["interactive", "hover", "image", "liquid", "distortion"],
|
|
2612
|
+
source: glsl(
|
|
2613
|
+
`// Liquid hover: an image bulging and rippling around the pointer, as strongly as it is hovered.
|
|
2614
|
+
uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
|
|
2615
|
+
uniform float uStrength; // @control min=0 max=1 default=0.6 label="Strength"
|
|
2616
|
+
uniform float uRadius; // @control min=0.05 max=0.8 default=0.3 label="Radius"
|
|
2617
|
+
uniform float uWobble; // @control min=0 max=1 default=0.5 label="Wobble"
|
|
2618
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2619
|
+
HASH,
|
|
2620
|
+
NOISE,
|
|
2621
|
+
HOVER,
|
|
2622
|
+
PICTURE,
|
|
2623
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2624
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2625
|
+
vec3 hp = hoverPoint(uDemo, 0.5);
|
|
2626
|
+
vec2 d = (fragCoord - hp.xy) / iResolution.y;
|
|
2627
|
+
float r = length(d);
|
|
2628
|
+
float s = uStrength * hp.z;
|
|
2629
|
+
float fall = exp(-pow(r / uRadius, 2.0) * 1.5);
|
|
2630
|
+
// Swell: pull samples towards the pointer, so it magnifies like a drop.
|
|
2631
|
+
vec2 off = -d * fall * s * 0.6;
|
|
2632
|
+
// Wobble: rings running out from the pointer and a slow liquid churn.
|
|
2633
|
+
vec2 dir = d / max(r, 1e-5);
|
|
2634
|
+
float wave = sin(r / uRadius * 14.0 - iTime * 5.0) * fall;
|
|
2635
|
+
off += dir * wave * uWobble * s * 0.02;
|
|
2636
|
+
vec2 churn = vec2(vnoise(d * 6.0 + iTime * 0.7), vnoise(d * 6.0 - iTime * 0.6 + 5.0)) - 0.5;
|
|
2637
|
+
off += churn * fall * s * uWobble * 0.05;
|
|
2638
|
+
vec3 col = picture(uImage, uv + off * vec2(iResolution.y / iResolution.x, 1.0));
|
|
2639
|
+
// Light catching the swell's upper-left slope.
|
|
2640
|
+
float slope = dot(dir, normalize(vec2(-0.6, 0.8))) * r / uRadius;
|
|
2641
|
+
col += 0.12 * s * fall * slope + 0.06 * s * wave * uWobble;
|
|
2642
|
+
fragColor = vec4(col, 1.0);
|
|
2643
|
+
}`
|
|
2644
|
+
),
|
|
2645
|
+
presets: [
|
|
2646
|
+
{ name: "Syrup", values: { uStrength: 0.9, uRadius: 0.45, uWobble: 0.15 } },
|
|
2647
|
+
{ name: "Splash", values: { uStrength: 0.5, uRadius: 0.25, uWobble: 1 } }
|
|
2648
|
+
]
|
|
2649
|
+
};
|
|
2650
|
+
var hoverReveal = {
|
|
2651
|
+
kind: "shader",
|
|
2652
|
+
id: "hover-reveal",
|
|
2653
|
+
name: "X-ray reveal",
|
|
2654
|
+
description: "A second image showing through a soft lens around the pointer that grows as it hovers and pulses on click.",
|
|
2655
|
+
category: "interaction",
|
|
2656
|
+
tags: ["interactive", "hover", "click", "image", "reveal", "x-ray"],
|
|
2657
|
+
source: glsl(
|
|
2658
|
+
`// X-ray reveal: a second image seen through a soft lens at the pointer.
|
|
2659
|
+
uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
|
|
2660
|
+
uniform sampler2D uReveal; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.city} label="Revealed image"
|
|
2661
|
+
uniform float uRadius; // @control min=0.05 max=0.6 default=0.22 label="Lens radius"
|
|
2662
|
+
uniform float uSoftness; // @control min=0 max=1 default=0.35 label="Softness"
|
|
2663
|
+
uniform vec3 uRim; // @control color default=#A5F3FC label="Rim"
|
|
2664
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2665
|
+
HASH,
|
|
2666
|
+
NOISE,
|
|
2667
|
+
CLICKS,
|
|
2668
|
+
HOVER,
|
|
2669
|
+
PICTURE,
|
|
2670
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2671
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2672
|
+
float px = 1.0 / iResolution.y;
|
|
2673
|
+
vec3 hp = hoverPoint(uDemo, 0.85);
|
|
2674
|
+
// Each click swells the lens, which settles back.
|
|
2675
|
+
float pulse = 0.0;
|
|
2676
|
+
for (int i = 0; i < 8; i++) {
|
|
2677
|
+
vec4 c = clickAt(i, uDemo);
|
|
2678
|
+
float age = iTime - c.z;
|
|
2679
|
+
if (c.w < 0.5 || age < 0.0) continue;
|
|
2680
|
+
pulse += 0.6 * exp(-age * 4.0) * (1.0 - exp(-age * 30.0));
|
|
2681
|
+
}
|
|
2682
|
+
float R = uRadius * hp.z * (1.0 + pulse);
|
|
2683
|
+
float d = length(fragCoord - hp.xy) * px;
|
|
2684
|
+
// A wobbly edge, so the lens looks like it is melting through.
|
|
2685
|
+
d += (vnoise(fragCoord * px * 9.0 + iTime * 0.6) - 0.5) * 0.25 * R;
|
|
2686
|
+
float soft = max(uSoftness * R * 0.6, 1.5 * px);
|
|
2687
|
+
float m = 1.0 - smoothstep(R - soft, R, d);
|
|
2688
|
+
vec3 col = mix(picture(uImage, uv), picture(uReveal, uv), m);
|
|
2689
|
+
float rim = exp(-pow((d - R + soft * 0.5) / max(soft * 0.5, 2.0 * px), 2.0)) * step(0.001, R);
|
|
2690
|
+
col += uRim * rim * 0.6;
|
|
2691
|
+
fragColor = vec4(col, 1.0);
|
|
2692
|
+
}`
|
|
2693
|
+
),
|
|
2694
|
+
presets: [
|
|
2695
|
+
{ name: "Peephole", values: { uRadius: 0.12, uSoftness: 0.05, uRim: [1, 1, 1] } },
|
|
2696
|
+
{ name: "Fog lamp", values: { uRadius: 0.4, uSoftness: 1, uRim: [0, 0, 0] } }
|
|
2697
|
+
]
|
|
2698
|
+
};
|
|
2699
|
+
var magnifier = {
|
|
2700
|
+
kind: "shader",
|
|
2701
|
+
id: "magnifier",
|
|
2702
|
+
name: "Magnifier",
|
|
2703
|
+
description: "A round magnifying glass over an image at the pointer, with a bright rim and a fringe of colour at its edge.",
|
|
2704
|
+
category: "interaction",
|
|
2705
|
+
tags: ["interactive", "hover", "image", "lens", "zoom"],
|
|
2706
|
+
source: glsl(
|
|
2707
|
+
`// Magnifier: a round lens over an image at the pointer, showing while hovered.
|
|
2708
|
+
uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.city} label="Image"
|
|
2709
|
+
uniform float uZoom; // @control min=1 max=5 default=2 label="Zoom"
|
|
2710
|
+
uniform float uRadius; // @control min=0.05 max=0.5 default=0.18 label="Radius"
|
|
2711
|
+
uniform float uAberration; // @control min=0 max=1 default=0.35 label="Colour fringe"
|
|
2712
|
+
uniform vec3 uRim; // @control color default=#FFFFFF label="Rim"
|
|
2713
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2714
|
+
HOVER,
|
|
2715
|
+
PICTURE,
|
|
2716
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2717
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2718
|
+
vec3 hp = hoverPoint(uDemo, 1.0);
|
|
2719
|
+
float s = hp.z;
|
|
2720
|
+
vec3 col = picture(uImage, uv);
|
|
2721
|
+
float R = uRadius * (0.75 + 0.25 * s) * iResolution.y;
|
|
2722
|
+
vec2 d = fragCoord - hp.xy;
|
|
2723
|
+
float r = length(d);
|
|
2724
|
+
float k = r / R;
|
|
2725
|
+
// A soft shadow under the glass.
|
|
2726
|
+
float shadow = 1.0 - smoothstep(R * 0.9, R * 1.3, length(d - vec2(R * 0.08, -R * 0.12)));
|
|
2727
|
+
col *= 1.0 - 0.35 * shadow * s;
|
|
2728
|
+
if (k < 1.05) {
|
|
2729
|
+
// More magnified at the centre, squeezed towards the rim like real glass.
|
|
2730
|
+
vec2 q = d / uZoom * (1.0 + 0.4 * k * k * k * k);
|
|
2731
|
+
float fr = uAberration * 0.08 * k * k * k;
|
|
2732
|
+
vec3 lens = vec3(
|
|
2733
|
+
picture(uImage, (hp.xy + q * (1.0 + fr)) / iResolution.xy).r,
|
|
2734
|
+
picture(uImage, (hp.xy + q) / iResolution.xy).g,
|
|
2735
|
+
picture(uImage, (hp.xy + q * (1.0 - fr)) / iResolution.xy).b
|
|
2736
|
+
);
|
|
2737
|
+
lens += 0.16 * exp(-pow(length(d / R - vec2(-0.35, 0.4)) * 2.6, 2.0));
|
|
2738
|
+
lens *= 1.0 - 0.25 * pow(k, 6.0);
|
|
2739
|
+
col = mix(col, lens, (1.0 - smoothstep(R - 1.5, R, r)) * s);
|
|
2740
|
+
}
|
|
2741
|
+
float rimW = max(R * 0.035, 2.0);
|
|
2742
|
+
float lit = 0.6 + 0.4 * dot(d / max(r, 1e-4), normalize(vec2(-1.0, 1.0)));
|
|
2743
|
+
float rim = exp(-pow((r - R) / rimW, 2.0));
|
|
2744
|
+
col = mix(col, uRim * lit, rim * s * 0.9);
|
|
2745
|
+
fragColor = vec4(col, 1.0);
|
|
2746
|
+
}`
|
|
2747
|
+
),
|
|
2748
|
+
presets: [
|
|
2749
|
+
{ name: "Loupe", values: { uZoom: 4, uRadius: 0.1, uAberration: 0.1 } },
|
|
2750
|
+
{ name: "Fishbowl", values: { uZoom: 1.5, uRadius: 0.35, uAberration: 1 } }
|
|
2751
|
+
]
|
|
2752
|
+
};
|
|
2753
|
+
var holoCard = {
|
|
2754
|
+
kind: "shader",
|
|
2755
|
+
id: "holo-card",
|
|
2756
|
+
name: "Holo card",
|
|
2757
|
+
description: "A holographic foil trading card whose rainbow sheen and glare shift as the pointer tilts it.",
|
|
2758
|
+
category: "interaction",
|
|
2759
|
+
tags: ["interactive", "hover", "card", "holographic", "foil", "iridescent"],
|
|
2760
|
+
transparent: true,
|
|
2761
|
+
source: glsl(
|
|
2762
|
+
`// Holo card: a rounded card with rainbow foil and glare that move as the pointer tilts it.
|
|
2763
|
+
uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
|
|
2764
|
+
uniform float uFoil; // @control min=0 max=1 default=0.7 label="Foil"
|
|
2765
|
+
uniform float uGlare; // @control min=0 max=1 default=0.6 label="Glare"
|
|
2766
|
+
uniform float uTilt; // @control min=0 max=1 default=0.6 label="Tilt"
|
|
2767
|
+
uniform float uCorner; // @control min=0 max=0.25 default=0.06 label="Corner radius"
|
|
2768
|
+
uniform vec3 uBorder; // @control color default=#E2E8F0 label="Border"
|
|
2769
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2770
|
+
HASH,
|
|
2771
|
+
NOISE,
|
|
2772
|
+
HOVER,
|
|
2773
|
+
OVER,
|
|
2774
|
+
PICTURE,
|
|
2775
|
+
`float roundedBox(vec2 p, vec2 hs, float r) {
|
|
2776
|
+
vec2 e = abs(p) - hs + r;
|
|
2777
|
+
return length(max(e, 0.0)) + min(max(e.x, e.y), 0.0) - r;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2781
|
+
vec2 res = iResolution.xy;
|
|
2782
|
+
const float aspect = 0.716;
|
|
2783
|
+
float h = min(res.y * 0.84, res.x * 0.84 / aspect);
|
|
2784
|
+
vec2 hs = vec2(h * aspect, h) * 0.5;
|
|
2785
|
+
vec3 hp = hoverPoint(uDemo, 0.7);
|
|
2786
|
+
vec2 tilt = clamp((hp.xy - 0.5 * res) / hs, -1.0, 1.0) * hp.z * uTilt;
|
|
2787
|
+
vec2 s = (fragCoord - 0.5 * res) / hs;
|
|
2788
|
+
// Fake perspective: the side the pointer is on leans away.
|
|
2789
|
+
vec2 q = s / max(1.0 - 0.15 * dot(s, tilt), 0.2);
|
|
2790
|
+
float radius = uCorner * hs.x * 2.0;
|
|
2791
|
+
float sd = roundedBox(q * hs, hs, radius);
|
|
2792
|
+
float mask = 1.0 - smoothstep(-1.0, 1.0, sd);
|
|
2793
|
+
// A soft shadow, falling away from the tilt.
|
|
2794
|
+
float shadowD = roundedBox(fragCoord - 0.5 * res + tilt * hs * 0.06 + vec2(0.0, h * 0.03), hs, radius);
|
|
2795
|
+
float shadow = (1.0 - smoothstep(-h * 0.04, h * 0.06, shadowD)) * 0.35;
|
|
2796
|
+
vec4 acc = over(vec4(0.0), vec3(0.0), shadow * (1.0 - mask));
|
|
2797
|
+
if (mask > 0.0) {
|
|
2798
|
+
vec2 cuv = q * 0.5 + 0.5;
|
|
2799
|
+
// The picture fills the card, cropped to fit.
|
|
2800
|
+
vec2 ts = vec2(textureSize(uImage, 0));
|
|
2801
|
+
float imageAspect = ts.x / max(ts.y, 1.0);
|
|
2802
|
+
vec2 fit = imageAspect > aspect ? vec2(aspect / imageAspect, 1.0) : vec2(1.0, imageAspect / aspect);
|
|
2803
|
+
vec3 base = picture(uImage, 0.5 + (cuv - 0.5) * fit);
|
|
2804
|
+
// Foil: rainbow bands that slide with the tilt, strongest in a broad band facing the light.
|
|
2805
|
+
float n = vnoise(cuv * 5.0);
|
|
2806
|
+
float phase = dot(cuv, vec2(0.7, 1.1)) * 1.6 + tilt.x * 0.9 - tilt.y * 0.6 + n * 0.5;
|
|
2807
|
+
vec3 rainbow = 0.5 + 0.5 * cos(6.28318 * (phase + vec3(0.0, 0.33, 0.67)));
|
|
2808
|
+
float lines = 0.75 + 0.25 * sin((cuv.x - cuv.y) * 90.0 + tilt.x * 8.0);
|
|
2809
|
+
float band = exp(-pow((cuv.x + cuv.y) * 0.7 - 0.7 - (tilt.x + tilt.y) * 0.6, 2.0) * 6.0);
|
|
2810
|
+
float foil = uFoil * (0.25 + 0.75 * band) * lines;
|
|
2811
|
+
// Overlay blend, so the picture keeps its darks under the foil.
|
|
2812
|
+
vec3 lit = mix(2.0 * base * rainbow, 1.0 - 2.0 * (1.0 - base) * (1.0 - rainbow), step(0.5, base));
|
|
2813
|
+
vec3 col = mix(base, lit + rainbow * 0.12, foil * 0.75);
|
|
2814
|
+
float sparkle = step(0.985, hash12(floor(cuv * vec2(90.0, 126.0))));
|
|
2815
|
+
col += sparkle * uFoil * band * (0.5 + 0.5 * sin(iTime * 3.0 + n * 20.0 + tilt.x * 10.0));
|
|
2816
|
+
// Glare: a highlight where the light hits, following the pointer.
|
|
2817
|
+
vec2 g = cuv - (0.5 + tilt * 0.55);
|
|
2818
|
+
col += uGlare * 0.6 * exp(-dot(g, g) * 7.0);
|
|
2819
|
+
// A thin foil border.
|
|
2820
|
+
float border = smoothstep(-0.03 * h - 1.0, -0.03 * h, sd);
|
|
2821
|
+
col = mix(col, uBorder * (0.8 + 0.4 * rainbow * uFoil), border);
|
|
2822
|
+
acc = over(acc, clamp(col, 0.0, 1.0), mask);
|
|
2823
|
+
}
|
|
2824
|
+
fragColor = unpremultiply(acc);
|
|
2825
|
+
}`
|
|
2826
|
+
),
|
|
2827
|
+
presets: [
|
|
2828
|
+
{ name: "Subtle", values: { uFoil: 0.3, uGlare: 0.35, uTilt: 0.4 } },
|
|
2829
|
+
{ name: "Full foil", values: { uFoil: 1, uGlare: 0.8, uTilt: 1, uBorder: [0.98, 0.82, 0.35] } }
|
|
2830
|
+
]
|
|
2831
|
+
};
|
|
2832
|
+
var pressCharge = {
|
|
2833
|
+
kind: "shader",
|
|
2834
|
+
id: "press-charge",
|
|
2835
|
+
name: "Press to charge",
|
|
2836
|
+
description: "A ring that fills and glows while the pointer is held and drains when it is let go, bursting if it was fully charged.",
|
|
2837
|
+
category: "interaction",
|
|
2838
|
+
tags: ["interactive", "press", "overlay", "hold", "button", "ring"],
|
|
2839
|
+
transparent: true,
|
|
2840
|
+
source: glsl(
|
|
2841
|
+
`// Press to charge: a ring that fills while held (iPointer.w > 0), drains when let go (iPointer.w < 0)
|
|
2842
|
+
// and bursts if it was let go fully charged.
|
|
2843
|
+
uniform vec3 uColor; // @control color default=#A78BFA label="Colour"
|
|
2844
|
+
uniform float uHold; // @control min=0.3 max=5 default=1.2 label="Seconds to charge"
|
|
2845
|
+
uniform float uSize; // @control min=0.08 max=0.45 default=0.22 label="Size"
|
|
2846
|
+
uniform float uThickness; // @control min=0.005 max=0.08 default=0.025 label="Thickness"
|
|
2847
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2848
|
+
OVER,
|
|
2849
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2850
|
+
float px = 1.0 / iResolution.y;
|
|
2851
|
+
// iPointer.w: seconds held while pressed, minus the seconds since letting go after, 0 before any press.
|
|
2852
|
+
float w = iPointer.w;
|
|
2853
|
+
// The press is the newest click, so this is how long it was held when let go.
|
|
2854
|
+
float heldFor = iTime + min(w, 0.0) - iClicks[0].z;
|
|
2855
|
+
// Until someone presses, a synthetic press: hold, charge, let go, burst, rest.
|
|
2856
|
+
if (uDemo && iClicks[0].w < 0.5 && w == 0.0) {
|
|
2857
|
+
float t = mod(iTime, uHold + 2.0) - 0.4;
|
|
2858
|
+
float hold = uHold + 0.4;
|
|
2859
|
+
w = t <= 0.0 ? 0.0 : (t < hold ? t : -(t - hold));
|
|
2860
|
+
heldFor = hold;
|
|
2861
|
+
}
|
|
2862
|
+
float held = max(w, 0.0);
|
|
2863
|
+
float since = max(-w, 0.0);
|
|
2864
|
+
float reached = w < 0.0 ? clamp(heldFor / uHold, 0.0, 1.0) : 0.0;
|
|
2865
|
+
// While held it fills; once let go it drains from where it got to, over 0.4 s.
|
|
2866
|
+
float drain = 1.0 - smoothstep(0.0, 0.4, since);
|
|
2867
|
+
float charge = w > 0.0 ? clamp(held / uHold, 0.0, 1.0) : reached * drain;
|
|
2868
|
+
float full = step(uHold, held);
|
|
2869
|
+
vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
|
|
2870
|
+
float r = length(p);
|
|
2871
|
+
float turn = fract(atan(p.x, p.y) / 6.28318 + 1.0);
|
|
2872
|
+
float R = uSize * (1.0 - 0.06 * charge + 0.03 * iPointer.z);
|
|
2873
|
+
float ringD = abs(r - R) - uThickness * 0.5;
|
|
2874
|
+
float ring = 1.0 - smoothstep(-px, px, ringD);
|
|
2875
|
+
vec4 acc = vec4(0.0);
|
|
2876
|
+
// The core fills faintly, and the track shows where the charge goes.
|
|
2877
|
+
acc = over(acc, uColor, (1.0 - smoothstep(R - px, R + px, r)) * 0.1 * charge);
|
|
2878
|
+
acc = over(acc, uColor * 0.6, ring * (0.22 + 0.1 * iPointer.z));
|
|
2879
|
+
// The charge: an arc clockwise from the top, glowing brighter as it fills.
|
|
2880
|
+
float arcEnd = (charge - turn) * 6.28318 * R / px;
|
|
2881
|
+
float arc = ring * clamp(arcEnd + 0.5, 0.0, 1.0) * step(0.001, charge);
|
|
2882
|
+
float pulse = 1.0 + full * 0.25 * sin(iTime * 9.0);
|
|
2883
|
+
float halo = exp(-max(ringD, 0.0) / (uThickness * 1.5)) * charge * charge * 0.6 * clamp(arcEnd + 0.5, 0.0, 1.0);
|
|
2884
|
+
acc = over(acc, uColor, clamp(halo * pulse, 0.0, 1.0));
|
|
2885
|
+
acc = over(acc, mix(uColor, vec3(1.0), 0.35 * charge * pulse), arc);
|
|
2886
|
+
// A ping when it fills, and a flash with a ring bursting out when let go fully charged.
|
|
2887
|
+
float flashAge = -1.0;
|
|
2888
|
+
float flashSize = 0.0;
|
|
2889
|
+
if (w > 0.0 && held >= uHold) { flashAge = held - uHold; flashSize = 0.5; }
|
|
2890
|
+
if (w < 0.0 && reached >= 1.0) { flashAge = since; flashSize = 1.0; }
|
|
2891
|
+
if (flashAge >= 0.0 && flashAge < 0.8) {
|
|
2892
|
+
float k = 1.0 - flashAge / 0.8;
|
|
2893
|
+
float burstR = R + flashAge * 0.6 * flashSize;
|
|
2894
|
+
float burst = exp(-pow((r - burstR) / (uThickness * (1.0 + flashAge * 3.0)), 2.0)) * k * k * flashSize;
|
|
2895
|
+
acc = over(acc, mix(uColor, vec3(1.0), 0.5), clamp(burst, 0.0, 1.0));
|
|
2896
|
+
acc = over(acc, vec3(1.0), exp(-r * r / (R * R * 0.5)) * exp(-flashAge * 6.0) * 0.8 * flashSize);
|
|
2897
|
+
}
|
|
2898
|
+
fragColor = unpremultiply(acc);
|
|
2899
|
+
}`
|
|
2900
|
+
),
|
|
2901
|
+
presets: [
|
|
2902
|
+
{ name: "Hold to confirm", values: { uColor: [0.95, 0.3, 0.3], uHold: 2, uSize: 0.15, uThickness: 0.015 } },
|
|
2903
|
+
{ name: "Power up", values: { uColor: [0.2, 0.9, 1], uHold: 0.8, uSize: 0.3, uThickness: 0.05 } }
|
|
2904
|
+
]
|
|
2905
|
+
};
|
|
2906
|
+
var hoverSwap = {
|
|
2907
|
+
kind: "shader",
|
|
2908
|
+
id: "hover-swap",
|
|
2909
|
+
name: "Hover swap",
|
|
2910
|
+
description: "One image burning away into another from the pointer as it hovers, along a glowing ragged edge.",
|
|
2911
|
+
category: "interaction",
|
|
2912
|
+
tags: ["interactive", "hover", "image", "dissolve", "transition"],
|
|
2913
|
+
source: glsl(
|
|
2914
|
+
`// Hover swap: image A dissolves into image B as hover goes 0 \u2192 1, spreading from the pointer.
|
|
2915
|
+
uniform sampler2D uFrom; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
|
|
2916
|
+
uniform sampler2D uTo; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.city} label="Hover image"
|
|
2917
|
+
uniform vec3 uEdge; // @control color default=#FFB86B label="Edge glow"
|
|
2918
|
+
uniform float uEdgeWidth; // @control min=0 max=0.3 default=0.08 label="Edge width"
|
|
2919
|
+
uniform float uGrain; // @control min=1 max=20 default=6 label="Noise scale"
|
|
2920
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2921
|
+
HASH,
|
|
2922
|
+
NOISE,
|
|
2923
|
+
FBM,
|
|
2924
|
+
HOVER,
|
|
2925
|
+
PICTURE,
|
|
2926
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2927
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2928
|
+
vec3 hp = hoverPoint(uDemo, 0.0);
|
|
2929
|
+
float h = clamp(iPointer.z, 0.0, 1.0);
|
|
2930
|
+
// Idle, hover swings slowly in and out.
|
|
2931
|
+
float demo = uDemo ? smoothstep(0.1, 0.9, 0.5 - 0.5 * cos(iTime * 0.9)) : 0.0;
|
|
2932
|
+
float progress = mix(demo, 1.0, h);
|
|
2933
|
+
// Distance from the pointer, as a share of the way to the farthest corner.
|
|
2934
|
+
vec2 o = hp.xy / iResolution.y;
|
|
2935
|
+
vec2 size = iResolution.xy / iResolution.y;
|
|
2936
|
+
float far = max(max(length(o), length(o - vec2(size.x, 0.0))), max(length(o - vec2(0.0, size.y)), length(o - size)));
|
|
2937
|
+
float dist = length(fragCoord / iResolution.y - o) / far;
|
|
2938
|
+
float f = 0.65 * dist + 0.35 * fbm(fragCoord / iResolution.y * uGrain, 5);
|
|
2939
|
+
float w = max(uEdgeWidth, 0.005);
|
|
2940
|
+
float t = progress * (1.0 + w);
|
|
2941
|
+
float m = 1.0 - smoothstep(t - w, t, f);
|
|
2942
|
+
vec3 col = mix(picture(uFrom, uv), picture(uTo, uv), m);
|
|
2943
|
+
float edge = m * (1.0 - m) * 4.0 * step(0.001, uEdgeWidth);
|
|
2944
|
+
col = mix(col, uEdge, edge * 0.85);
|
|
2945
|
+
fragColor = vec4(col, 1.0);
|
|
2946
|
+
}`
|
|
2947
|
+
),
|
|
2948
|
+
presets: [
|
|
2949
|
+
{ name: "Clean cut", values: { uEdgeWidth: 0.01, uGrain: 2 } },
|
|
2950
|
+
{ name: "Embers", values: { uEdge: [1, 0.35, 0.1], uEdgeWidth: 0.2, uGrain: 14 } }
|
|
2951
|
+
]
|
|
2952
|
+
};
|
|
2953
|
+
var interactionShaders = [
|
|
2954
|
+
clickRipple,
|
|
2955
|
+
clickBurst,
|
|
2956
|
+
cursorGlow,
|
|
2957
|
+
hoverLiquid,
|
|
2958
|
+
hoverReveal,
|
|
2959
|
+
magnifier,
|
|
2960
|
+
holoCard,
|
|
2961
|
+
pressCharge,
|
|
2962
|
+
hoverSwap
|
|
2963
|
+
];
|
|
2964
|
+
|
|
1884
2965
|
// src/catalog/shaders/morph.ts
|
|
1885
2966
|
var pair = (from, to) => `uniform sampler2D uFrom; // @control filter=linear wrap=clamp default=${from} label="From image"
|
|
1886
2967
|
uniform sampler2D uTo; // @control filter=linear wrap=clamp default=${to} label="To image"
|
|
@@ -2330,13 +3411,13 @@ var morphShaders = [
|
|
|
2330
3411
|
];
|
|
2331
3412
|
|
|
2332
3413
|
// src/catalog/shaders/overlay.ts
|
|
2333
|
-
var
|
|
3414
|
+
var GLOW2 = `
|
|
2334
3415
|
vec4 glow(vec3 c) {
|
|
2335
3416
|
c = 1.0 - exp(-max(c, 0.0));
|
|
2336
3417
|
float a = max(c.r, max(c.g, c.b));
|
|
2337
3418
|
return vec4(c / max(a, 1e-4), a);
|
|
2338
3419
|
}`.trim();
|
|
2339
|
-
var
|
|
3420
|
+
var OVER2 = `
|
|
2340
3421
|
vec4 over(vec4 acc, vec3 c, float a) {
|
|
2341
3422
|
return vec4(acc.rgb * (1.0 - a) + c * a, acc.a * (1.0 - a) + a);
|
|
2342
3423
|
}
|
|
@@ -2358,7 +3439,7 @@ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
|
|
|
2358
3439
|
uniform float uSize; // @control min=0.5 max=2 default=1 label="Drop size"
|
|
2359
3440
|
uniform vec3 uTint; // @control color default=#CFE3F0 label="Tint"`,
|
|
2360
3441
|
HASH,
|
|
2361
|
-
|
|
3442
|
+
OVER2,
|
|
2362
3443
|
`// One drop of radius r at offset d: a faint tinted body, a darker rim and a bright highlight.
|
|
2363
3444
|
vec4 paintDrop(vec4 acc, vec2 d, float r, float px) {
|
|
2364
3445
|
float dist = length(d);
|
|
@@ -2492,7 +3573,7 @@ uniform vec3 uColor; // @control color default=#FFC98A label="Colour"
|
|
|
2492
3573
|
uniform float uStreak; // @control min=0 max=2 default=1 label="Streak length"
|
|
2493
3574
|
uniform float uIntensity; // @control min=0 max=2 default=1 label="Intensity"`,
|
|
2494
3575
|
HASH,
|
|
2495
|
-
|
|
3576
|
+
GLOW2,
|
|
2496
3577
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2497
3578
|
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
|
2498
3579
|
float aspect = iResolution.x / iResolution.y;
|
|
@@ -2542,7 +3623,7 @@ uniform float uSoftness; // @control min=0.05 max=1 default=0.5 label="Softness
|
|
|
2542
3623
|
uniform float uGrain; // @control min=0 max=0.3 default=0.08 label="Grain"
|
|
2543
3624
|
uniform float uGrainSize; // @control min=1 max=4 default=1.5 label="Grain size (px)"`,
|
|
2544
3625
|
HASH,
|
|
2545
|
-
|
|
3626
|
+
OVER2,
|
|
2546
3627
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2547
3628
|
float aspect = iResolution.x / iResolution.y;
|
|
2548
3629
|
// 0 at the centre, 1 at the edges; rounder shapes measure across the frame's aspect.
|
|
@@ -2579,7 +3660,7 @@ uniform float uWidth; // @control min=0.001 max=0.05 default=0.006 label="Wi
|
|
|
2579
3660
|
uniform vec3 uColor; // @control color default=#4FF0FF label="Colour"
|
|
2580
3661
|
uniform float uInterval; // @control min=0 max=5 default=1 label="Pause (s)"
|
|
2581
3662
|
uniform float uTrail; // @control min=0 max=1 default=0.5 label="Trail"`,
|
|
2582
|
-
|
|
3663
|
+
GLOW2,
|
|
2583
3664
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2584
3665
|
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
|
2585
3666
|
float aspect = iResolution.x / iResolution.y;
|
|
@@ -2653,7 +3734,7 @@ uniform vec3 uHeat; // @control color default=#FF7A1A label="Heat"
|
|
|
2653
3734
|
uniform float uSpeed; // @control min=0 max=3 default=1 label="Rise speed"
|
|
2654
3735
|
uniform float uSize; // @control min=0.3 max=3 default=1 label="Size"`,
|
|
2655
3736
|
HASH,
|
|
2656
|
-
|
|
3737
|
+
GLOW2,
|
|
2657
3738
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2658
3739
|
vec2 p = fragCoord / iResolution.y;
|
|
2659
3740
|
float aspect = iResolution.x / iResolution.y;
|
|
@@ -3705,24 +4786,24 @@ var imageTreatments = [
|
|
|
3705
4786
|
];
|
|
3706
4787
|
|
|
3707
4788
|
// src/catalog/text.ts
|
|
3708
|
-
var
|
|
3709
|
-
function
|
|
4789
|
+
var r12 = (n4) => Math.round(n4 * 10) / 10;
|
|
4790
|
+
function fit2(text, width, max, ems = textWidthEm(text)) {
|
|
3710
4791
|
const e = Math.max(0.6, ems);
|
|
3711
4792
|
const size = Math.min(max, width / e);
|
|
3712
|
-
return { size:
|
|
4793
|
+
return { size: r12(size), length: r12(e * size), left: r12(200 - e * size / 2) };
|
|
3713
4794
|
}
|
|
3714
4795
|
var lettersOf = (text) => Array.from(String(text));
|
|
3715
|
-
var
|
|
4796
|
+
var mixHex2 = (a, b, t) => {
|
|
3716
4797
|
const x = Array.isArray(a) ? a : [1, 1, 1];
|
|
3717
4798
|
const y = Array.isArray(b) ? b : [1, 1, 1];
|
|
3718
4799
|
return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
|
|
3719
4800
|
};
|
|
3720
4801
|
var holds = (id, attr, keys) => keys.map(([delay, value]) => ({ id, attrs: { [attr]: [value, value] }, delay, ms: 0 }));
|
|
3721
|
-
var
|
|
4802
|
+
var n3 = (name) => `{{${name}}}`;
|
|
3722
4803
|
var MONO = `ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
|
|
3723
4804
|
var MONO_EM = 0.6;
|
|
3724
4805
|
var HEAVY = `'Arial Black', 'Helvetica Neue', Impact, system-ui, sans-serif`;
|
|
3725
|
-
var
|
|
4806
|
+
var SANS2 = `system-ui, 'Helvetica Neue', Arial, sans-serif`;
|
|
3726
4807
|
var BLINKS = 7;
|
|
3727
4808
|
var typewriter = {
|
|
3728
4809
|
kind: "vector",
|
|
@@ -3739,14 +4820,14 @@ var typewriter = {
|
|
|
3739
4820
|
// Monospace, so the caret lands after the last letter however long the text is.
|
|
3740
4821
|
derived: (v) => {
|
|
3741
4822
|
const count = Math.max(1, lettersOf(v.text).length);
|
|
3742
|
-
const size =
|
|
3743
|
-
const left =
|
|
4823
|
+
const size = r12(Math.min(44, 340 / ((count + 1) * MONO_EM)));
|
|
4824
|
+
const left = r12(200 - (count + 1) * MONO_EM * size / 2);
|
|
3744
4825
|
const typing = Math.round(count * Number(v.pace));
|
|
3745
4826
|
return {
|
|
3746
4827
|
size,
|
|
3747
4828
|
left,
|
|
3748
|
-
base:
|
|
3749
|
-
end:
|
|
4829
|
+
base: r12(100 + size * 0.35),
|
|
4830
|
+
end: r12(left + count * MONO_EM * size),
|
|
3750
4831
|
typing,
|
|
3751
4832
|
...Object.fromEntries(Array.from({ length: BLINKS }, (_, k) => [`blink${k}`, typing + k * 500]))
|
|
3752
4833
|
};
|
|
@@ -3754,9 +4835,9 @@ var typewriter = {
|
|
|
3754
4835
|
motion: {
|
|
3755
4836
|
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>`,
|
|
3756
4837
|
animate: [
|
|
3757
|
-
{ id: "typed", write: "{{text}}", caret: "\u258C", ms:
|
|
4838
|
+
{ id: "typed", write: "{{text}}", caret: "\u258C", ms: n3("typing"), ease: "linear" },
|
|
3758
4839
|
// Hidden while the writer shows its own caret, then blinking where it stopped.
|
|
3759
|
-
...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [
|
|
4840
|
+
...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n3(`blink${k}`), k % 2 ? 0 : 1])])
|
|
3760
4841
|
]
|
|
3761
4842
|
}
|
|
3762
4843
|
};
|
|
@@ -3774,17 +4855,17 @@ var scrambleText = {
|
|
|
3774
4855
|
],
|
|
3775
4856
|
derived: (v) => {
|
|
3776
4857
|
const count = Math.max(1, lettersOf(v.text).length);
|
|
3777
|
-
const size =
|
|
3778
|
-
const width =
|
|
3779
|
-
const base =
|
|
3780
|
-
return { size, base, barX:
|
|
4858
|
+
const size = r12(Math.min(60, 360 / (count * MONO_EM)));
|
|
4859
|
+
const width = r12(count * MONO_EM * size);
|
|
4860
|
+
const base = r12(100 + size * 0.35);
|
|
4861
|
+
return { size, base, barX: r12(200 - width / 2), barY: r12(base + size * 0.3), barW: width, decode: Math.round(count * Number(v.pace)), glowWide: Number(v.glow) * 2 };
|
|
3781
4862
|
},
|
|
3782
4863
|
motion: {
|
|
3783
4864
|
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>`,
|
|
3784
4865
|
animate: [
|
|
3785
4866
|
// Pure noise for a moment (the scramble runs before the change starts), then it decodes.
|
|
3786
|
-
{ id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms:
|
|
3787
|
-
{ id: "bar", attrs: { width: [0,
|
|
4867
|
+
{ id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n3("decode"), ease: "linear" },
|
|
4868
|
+
{ id: "bar", attrs: { width: [0, n3("barW")] }, delay: 500, ms: n3("decode"), ease: "linear" }
|
|
3788
4869
|
]
|
|
3789
4870
|
}
|
|
3790
4871
|
};
|
|
@@ -3810,20 +4891,20 @@ var kineticStagger = {
|
|
|
3810
4891
|
const base = 100 + size * 0.35;
|
|
3811
4892
|
const stagger = Number(v.stagger);
|
|
3812
4893
|
const out = {
|
|
3813
|
-
size:
|
|
3814
|
-
base:
|
|
3815
|
-
rise:
|
|
3816
|
-
drop:
|
|
3817
|
-
lineX:
|
|
3818
|
-
lineY:
|
|
3819
|
-
lineW:
|
|
4894
|
+
size: r12(size),
|
|
4895
|
+
base: r12(base),
|
|
4896
|
+
rise: r12(base - size * 0.1),
|
|
4897
|
+
drop: r12(base + size * 0.6),
|
|
4898
|
+
lineX: r12(200 - ems * size / 2),
|
|
4899
|
+
lineY: r12(base + size * 0.2),
|
|
4900
|
+
lineW: r12(ems * size),
|
|
3820
4901
|
lineAt: Math.round(letters.length * stagger + 400)
|
|
3821
4902
|
};
|
|
3822
4903
|
let x = 200 - ems * size / 2;
|
|
3823
4904
|
for (let i = 0; i < SLOTS2; i++) {
|
|
3824
4905
|
const w = (widths[i] ?? 0) * size;
|
|
3825
4906
|
out[`c${i}`] = letters[i]?.trim() ?? "";
|
|
3826
|
-
out[`x${i}`] =
|
|
4907
|
+
out[`x${i}`] = r12(x + w / 2);
|
|
3827
4908
|
out[`d${i}`] = Math.round(i * stagger);
|
|
3828
4909
|
out[`e${i}`] = Math.round(i * stagger + 380);
|
|
3829
4910
|
x += w;
|
|
@@ -3840,13 +4921,13 @@ var kineticStagger = {
|
|
|
3840
4921
|
{
|
|
3841
4922
|
id: `l${i}`,
|
|
3842
4923
|
attrs: { transform: [`translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)`, `translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`], opacity: [0, 1] },
|
|
3843
|
-
delay:
|
|
4924
|
+
delay: n3(`d${i}`),
|
|
3844
4925
|
ms: 380,
|
|
3845
4926
|
ease: "out"
|
|
3846
4927
|
},
|
|
3847
|
-
{ id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay:
|
|
4928
|
+
{ id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n3(`e${i}`), ms: 260, ease: "in-out" }
|
|
3848
4929
|
]).flat(),
|
|
3849
|
-
{ id: "line", attrs: { width: [0,
|
|
4930
|
+
{ id: "line", attrs: { width: [0, n3("lineW")] }, delay: n3("lineAt"), ms: 500, ease: "in-out" }
|
|
3850
4931
|
]
|
|
3851
4932
|
}
|
|
3852
4933
|
};
|
|
@@ -3867,16 +4948,16 @@ var neonSign = {
|
|
|
3867
4948
|
derived: (v) => {
|
|
3868
4949
|
const letters = lettersOf(v.text);
|
|
3869
4950
|
const k = Math.min(Math.max(0, Math.round(Number(v.faulty)) - 1), Math.max(0, letters.length - 1));
|
|
3870
|
-
const { size, length } =
|
|
4951
|
+
const { size, length } = fit2(String(v.text), 340, 84);
|
|
3871
4952
|
return {
|
|
3872
4953
|
head: letters.slice(0, k).join(""),
|
|
3873
4954
|
mid: letters[k] ?? "",
|
|
3874
4955
|
tail: letters.slice(k + 1).join(""),
|
|
3875
4956
|
size,
|
|
3876
4957
|
length,
|
|
3877
|
-
base:
|
|
4958
|
+
base: r12(100 + size * 0.3),
|
|
3878
4959
|
far: Number(v.glow) * 3,
|
|
3879
|
-
core:
|
|
4960
|
+
core: mixHex2(v.tube, [1, 1, 1], 0.6)
|
|
3880
4961
|
};
|
|
3881
4962
|
},
|
|
3882
4963
|
motion: (() => {
|
|
@@ -3908,14 +4989,14 @@ var strokeDrawText = {
|
|
|
3908
4989
|
],
|
|
3909
4990
|
// <text> takes no pathLength, so `draw` cannot measure it: the dash grows instead, longer than any letter's outline.
|
|
3910
4991
|
derived: (v) => {
|
|
3911
|
-
const { size, length } =
|
|
3912
|
-
return { size, length, base:
|
|
4992
|
+
const { size, length } = fit2(String(v.text), 360, 100);
|
|
4993
|
+
return { size, length, base: r12(100 + size * 0.35), dash: Math.round(size * 6) };
|
|
3913
4994
|
},
|
|
3914
4995
|
motion: {
|
|
3915
4996
|
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>`,
|
|
3916
4997
|
animate: [
|
|
3917
4998
|
{ id: "word", attrs: { "stroke-dasharray": ["0 {{dash}}", "{{dash}} 0"] }, ms: 2e3, ease: "in-out" },
|
|
3918
|
-
{ id: "word", attrs: { "fill-opacity": [0,
|
|
4999
|
+
{ id: "word", attrs: { "fill-opacity": [0, n3("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
|
|
3919
5000
|
]
|
|
3920
5001
|
}
|
|
3921
5002
|
};
|
|
@@ -3932,21 +5013,21 @@ var splitReveal = {
|
|
|
3932
5013
|
{ name: "Gold on black", values: { background: [0.02, 0.02, 0.02], ink: [0.96, 0.84, 0.55], line: [0.96, 0.84, 0.55] } }
|
|
3933
5014
|
],
|
|
3934
5015
|
derived: (v) => {
|
|
3935
|
-
const { size, length, left } =
|
|
5016
|
+
const { size, length, left } = fit2(String(v.text), 360, 88);
|
|
3936
5017
|
return {
|
|
3937
5018
|
size,
|
|
3938
5019
|
length,
|
|
3939
|
-
base:
|
|
3940
|
-
shift:
|
|
3941
|
-
lineX1:
|
|
3942
|
-
lineX2:
|
|
3943
|
-
captionY:
|
|
5020
|
+
base: r12(100 + size * 0.36),
|
|
5021
|
+
shift: r12(size * 0.8),
|
|
5022
|
+
lineX1: r12(left - 10),
|
|
5023
|
+
lineX2: r12(400 - left + 10),
|
|
5024
|
+
captionY: r12(100 + size * 0.36 + 30)
|
|
3944
5025
|
};
|
|
3945
5026
|
},
|
|
3946
5027
|
motion: (() => {
|
|
3947
|
-
const word = `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5028
|
+
const word = `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="800" fill="{{ink}}">{{text}}</text>`;
|
|
3948
5029
|
return {
|
|
3949
|
-
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} --><defs><clipPath id="above"><rect width="400" height="100"/></clipPath><clipPath id="below"><rect y="100" width="400" height="100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="${
|
|
5030
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} --><defs><clipPath id="above"><rect width="400" height="100"/></clipPath><clipPath id="below"><rect y="100" width="400" height="100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="${SANS2}" font-size="13" letter-spacing="3" fill="{{ink}}" opacity="0">{{caption}}</text></svg>`,
|
|
3950
5031
|
animate: [
|
|
3951
5032
|
{ id: "line", draw: true, ms: 600, ease: "in-out" },
|
|
3952
5033
|
{ id: "top", attrs: { transform: ["translate(0 {{shift}})", "translate(0 0)"] }, delay: 450, ms: 800, ease: "out" },
|
|
@@ -3972,7 +5053,7 @@ var highlighter = {
|
|
|
3972
5053
|
],
|
|
3973
5054
|
// The swipe runs a little past the phrase at both ends, across its lower half, with a ragged second pass.
|
|
3974
5055
|
derived: (v) => {
|
|
3975
|
-
const { size, length, left } =
|
|
5056
|
+
const { size, length, left } = fit2(String(v.text), 330, 56);
|
|
3976
5057
|
const base = 100 + size * 0.35;
|
|
3977
5058
|
const x0 = left - size * 0.25;
|
|
3978
5059
|
const x1 = 400 - left + size * 0.2;
|
|
@@ -3980,15 +5061,15 @@ var highlighter = {
|
|
|
3980
5061
|
return {
|
|
3981
5062
|
size,
|
|
3982
5063
|
length,
|
|
3983
|
-
base:
|
|
3984
|
-
swipe: `M${
|
|
3985
|
-
ragged: `M${
|
|
3986
|
-
thick:
|
|
3987
|
-
thin:
|
|
5064
|
+
base: r12(base),
|
|
5065
|
+
swipe: `M${r12(x0)} ${r12(y + size * 0.05)} Q${r12((x0 + x1) / 2)} ${r12(y - size * 0.06)} ${r12(x1)} ${r12(y - size * 0.02)}`,
|
|
5066
|
+
ragged: `M${r12(x0 + size * 0.15)} ${r12(y + size * 0.3)} L${r12(x1 - size * 0.1)} ${r12(y + size * 0.26)}`,
|
|
5067
|
+
thick: r12(size * 0.72),
|
|
5068
|
+
thin: r12(size * 0.16)
|
|
3988
5069
|
};
|
|
3989
5070
|
},
|
|
3990
5071
|
motion: {
|
|
3991
|
-
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} --><rect width="400" height="200" fill="{{background}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5072
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} --><rect width="400" height="200" fill="{{background}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="700" fill="{{ink}}" opacity="0">{{text}}</text></svg>`,
|
|
3992
5073
|
animate: [
|
|
3993
5074
|
{ id: "phrase", attrs: { opacity: [0, 1] }, ms: 300, ease: "out" },
|
|
3994
5075
|
{ id: "swipe", draw: true, delay: 350, ms: 650, ease: "in-out" },
|
|
@@ -4023,36 +5104,36 @@ var wordCycle = {
|
|
|
4023
5104
|
const step = size * 1.3;
|
|
4024
5105
|
const wx = left + (lead + 0.3) * size;
|
|
4025
5106
|
return {
|
|
4026
|
-
size:
|
|
4027
|
-
base:
|
|
4028
|
-
px:
|
|
4029
|
-
plen:
|
|
4030
|
-
wx:
|
|
4031
|
-
clipX:
|
|
4032
|
-
clipY:
|
|
4033
|
-
clipW:
|
|
4034
|
-
clipH:
|
|
4035
|
-
underY:
|
|
5107
|
+
size: r12(size),
|
|
5108
|
+
base: r12(base),
|
|
5109
|
+
px: r12(left),
|
|
5110
|
+
plen: r12(Math.max(0.6, lead) * size),
|
|
5111
|
+
wx: r12(wx),
|
|
5112
|
+
clipX: r12(wx - 4),
|
|
5113
|
+
clipY: r12(base - size * 1),
|
|
5114
|
+
clipW: r12(Math.max(...ems) * size + 8),
|
|
5115
|
+
clipH: r12(size * 1.32),
|
|
5116
|
+
underY: r12(base + size * 0.18),
|
|
4036
5117
|
...Object.fromEntries(
|
|
4037
5118
|
[...words, words[0]].flatMap((_, k) => [
|
|
4038
|
-
[`y${k}`,
|
|
4039
|
-
[`o${k}`,
|
|
4040
|
-
[`len${k}`,
|
|
5119
|
+
[`y${k}`, r12(base + k * step)],
|
|
5120
|
+
[`o${k}`, r12(k * step)],
|
|
5121
|
+
[`len${k}`, r12(ems[k % CYCLE_WORDS] * size)]
|
|
4041
5122
|
])
|
|
4042
5123
|
),
|
|
4043
5124
|
...Object.fromEntries(words.map((w, k) => [`w${k}`, w]))
|
|
4044
5125
|
};
|
|
4045
5126
|
},
|
|
4046
5127
|
motion: {
|
|
4047
|
-
svg: `<svg viewBox="0 0 400 200"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5128
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="700" fill="{{ink}}">{{prefix}}</text><g clip-path="url(#window)"><g id="reel" transform="translate(0 -0)">${Array.from(
|
|
4048
5129
|
{ length: CYCLE_WORDS + 1 },
|
|
4049
|
-
(_, k) => `<text x="{{wx}}" y="{{y${k}}}" textLength="{{len${k}}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5130
|
+
(_, k) => `<text x="{{wx}}" y="{{y${k}}}" textLength="{{len${k}}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="800" fill="{{accent}}">{{w${k % CYCLE_WORDS}}}</text>`
|
|
4050
5131
|
).join("")}</g></g><rect id="under" x="{{wx}}" y="{{underY}}" width="{{len0}}" height="4" rx="2" fill="{{accent}}"/></svg>`,
|
|
4051
5132
|
animate: Array.from({ length: CYCLE_WORDS }, (_, i) => {
|
|
4052
5133
|
const at = { delay: (i + 1) * SHOW_MS + i * ROLL_MS, ms: ROLL_MS, ease: "in-out" };
|
|
4053
5134
|
return [
|
|
4054
5135
|
{ id: "reel", attrs: { transform: [`translate(0 -{{o${i}}})`, `translate(0 -{{o${i + 1}}})`] }, ...at },
|
|
4055
|
-
{ id: "under", attrs: { width: [
|
|
5136
|
+
{ id: "under", attrs: { width: [n3(`len${i}`), n3(`len${i + 1}`)] }, ...at }
|
|
4056
5137
|
];
|
|
4057
5138
|
}).flat()
|
|
4058
5139
|
}
|
|
@@ -4073,9 +5154,9 @@ var liquidFill = {
|
|
|
4073
5154
|
],
|
|
4074
5155
|
// The surface starts under the letters and ends above their caps.
|
|
4075
5156
|
derived: (v) => {
|
|
4076
|
-
const { size, length } =
|
|
5157
|
+
const { size, length } = fit2(String(v.text), 360, 110);
|
|
4077
5158
|
const base = 100 + size * 0.36;
|
|
4078
|
-
return { size, length, base:
|
|
5159
|
+
return { size, length, base: r12(base), low: r12(base + 12), high: r12(base - size * 0.82 - 12) };
|
|
4079
5160
|
},
|
|
4080
5161
|
motion: (() => {
|
|
4081
5162
|
const word = `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${HEAVY}" font-size="{{size}}" font-weight="900"`;
|
|
@@ -4122,17 +5203,17 @@ var glitchText = {
|
|
|
4122
5203
|
{ name: "Vapor", values: { left: [1, 0.3, 0.85], right: [0.4, 0.6, 1], background: [0.08, 0.02, 0.12], amount: 1.4 } }
|
|
4123
5204
|
],
|
|
4124
5205
|
derived: (v) => {
|
|
4125
|
-
const { size, length } =
|
|
5206
|
+
const { size, length } = fit2(String(v.text), 340, 100);
|
|
4126
5207
|
const base = 100 + size * 0.36;
|
|
4127
5208
|
const top = base - size * 0.78;
|
|
4128
|
-
const cuts = [0, ...BANDS.map((f) =>
|
|
5209
|
+
const cuts = [0, ...BANDS.map((f) => r12(top + f * (base - top))), 200];
|
|
4129
5210
|
const unit = size * Number(v.amount);
|
|
4130
|
-
const shift = ([x, y]) => `translate(${
|
|
5211
|
+
const shift = ([x, y]) => `translate(${r12(x * unit)} ${r12(y * unit)})`;
|
|
4131
5212
|
return {
|
|
4132
5213
|
size,
|
|
4133
5214
|
length,
|
|
4134
|
-
base:
|
|
4135
|
-
...Object.fromEntries([0, 1, 2, 3].flatMap((b) => [[`cut${b}`, cuts[b]], [`band${b}`,
|
|
5215
|
+
base: r12(base),
|
|
5216
|
+
...Object.fromEntries([0, 1, 2, 3].flatMap((b) => [[`cut${b}`, cuts[b]], [`band${b}`, r12(cuts[b + 1] - cuts[b])]])),
|
|
4136
5217
|
...Object.fromEntries(
|
|
4137
5218
|
GLITCH.flatMap((key, k) => [
|
|
4138
5219
|
[`red${k}`, shift(key.red)],
|
|
@@ -4172,21 +5253,21 @@ var extrudedText = {
|
|
|
4172
5253
|
],
|
|
4173
5254
|
// Each layer sits a step further back along the extrude direction, shaded from the side colour to the shadow.
|
|
4174
5255
|
derived: (v) => {
|
|
4175
|
-
const { size, length } =
|
|
5256
|
+
const { size, length } = fit2(String(v.text), 320, 96);
|
|
4176
5257
|
const step = size * 0.03 * Number(v.depth);
|
|
4177
5258
|
const at = (deg, i) => {
|
|
4178
5259
|
const a = deg * Math.PI / 180;
|
|
4179
|
-
return `translate(${
|
|
5260
|
+
return `translate(${r12(Math.cos(a) * step * i)} ${r12(Math.sin(a) * step * i)})`;
|
|
4180
5261
|
};
|
|
4181
5262
|
return {
|
|
4182
5263
|
size,
|
|
4183
5264
|
length,
|
|
4184
|
-
base:
|
|
5265
|
+
base: r12(92 + size * 0.35),
|
|
4185
5266
|
...Object.fromEntries(
|
|
4186
5267
|
Array.from({ length: LAYERS }, (_, k) => {
|
|
4187
5268
|
const i = LAYERS - k;
|
|
4188
5269
|
return [
|
|
4189
|
-
[`fill${i}`,
|
|
5270
|
+
[`fill${i}`, mixHex2(v.side, v.shade, i / LAYERS)],
|
|
4190
5271
|
[`a${i}`, at(EXTRUDE_FROM, i)],
|
|
4191
5272
|
[`b${i}`, at(EXTRUDE_TO, i)]
|
|
4192
5273
|
];
|
|
@@ -4233,6 +5314,7 @@ var EFFECT_CATEGORIES = [
|
|
|
4233
5314
|
"overlay",
|
|
4234
5315
|
"text",
|
|
4235
5316
|
"morph",
|
|
5317
|
+
"interaction",
|
|
4236
5318
|
"transition",
|
|
4237
5319
|
"loader",
|
|
4238
5320
|
"data",
|
|
@@ -4915,9 +5997,10 @@ var SHADER_EFFECTS = [
|
|
|
4915
5997
|
...overlayEffects,
|
|
4916
5998
|
...textShaders,
|
|
4917
5999
|
...morphShaders,
|
|
6000
|
+
...interactionShaders,
|
|
4918
6001
|
...transitions
|
|
4919
6002
|
];
|
|
4920
|
-
var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects].sort(
|
|
6003
|
+
var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects].sort(
|
|
4921
6004
|
(a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
|
|
4922
6005
|
);
|
|
4923
6006
|
var byId = new Map(EFFECTS.map((e) => [e.id, e]));
|
|
@@ -4970,4 +6053,4 @@ export {
|
|
|
4970
6053
|
effectControls,
|
|
4971
6054
|
effectDefaults
|
|
4972
6055
|
};
|
|
4973
|
-
//# sourceMappingURL=chunk-
|
|
6056
|
+
//# sourceMappingURL=chunk-STGGN4MR.js.map
|