use-scroll-animate 5.6.0 → 5.8.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 +23 -0
- package/dist/components/effects.cjs +758 -1
- package/dist/components/effects.cjs.map +1 -1
- package/dist/components/effects.d.cts +185 -2
- package/dist/components/effects.d.ts +185 -2
- package/dist/components/effects.js +739 -4
- package/dist/components/effects.js.map +1 -1
- package/dist/components.umd.js +2 -2
- package/dist/components.umd.js.map +1 -1
- package/docs/ROADMAP.md +2 -2
- package/docs/components.md +36 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { playEffect, registerBuiltinEffects, registerEffects } from './fx.js';
|
|
2
|
-
import { x as defineElement, p as prefersReducedMotion } from '../chunks/base-C3Sw9sAO.js';
|
|
1
|
+
import { playEffect, bindEffect, registerBuiltinEffects, registerEffects } from './fx.js';
|
|
2
|
+
import { x as defineElement, p as prefersReducedMotion, u as adoptStyles } from '../chunks/base-C3Sw9sAO.js';
|
|
3
|
+
import { applyMotionTokens, motionTokensToVars, mergeMotionTokens } from './tokens.js';
|
|
3
4
|
import '../chunks/core-DN3hHbHh.js';
|
|
4
|
-
import './tokens.js';
|
|
5
5
|
import '../chunks/fx-ChjxrMBo.js';
|
|
6
6
|
|
|
7
7
|
let layer = null;
|
|
@@ -1439,6 +1439,727 @@ function defineAudio(tag = 'usa-audio') {
|
|
|
1439
1439
|
}, { id: 'usa-audio', text: 'usa-audio{display:block}usa-audio[data-audio-error] [data-audio-toggle]{outline:2px solid #ff5c8a}' });
|
|
1440
1440
|
}
|
|
1441
1441
|
|
|
1442
|
+
const touchOk = (e, o) => o.touch || e.pointerType !== 'touch';
|
|
1443
|
+
/** A fixed, viewport-sized canvas in the fx layer, redrawn while `draw` returns true. */
|
|
1444
|
+
function overlayCanvas(draw) {
|
|
1445
|
+
const c = document.createElement('canvas');
|
|
1446
|
+
c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%';
|
|
1447
|
+
fxLayer().appendChild(c);
|
|
1448
|
+
const g = c.getContext('2d');
|
|
1449
|
+
let raf = 0;
|
|
1450
|
+
const frame = (now) => {
|
|
1451
|
+
raf = 0;
|
|
1452
|
+
if (!g)
|
|
1453
|
+
return;
|
|
1454
|
+
const w = innerWidth;
|
|
1455
|
+
const h = innerHeight;
|
|
1456
|
+
const dpr = Math.min(2, devicePixelRatio || 1);
|
|
1457
|
+
if (c.width !== Math.round(w * dpr) || c.height !== Math.round(h * dpr)) {
|
|
1458
|
+
c.width = Math.round(w * dpr);
|
|
1459
|
+
c.height = Math.round(h * dpr);
|
|
1460
|
+
}
|
|
1461
|
+
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1462
|
+
g.clearRect(0, 0, w, h);
|
|
1463
|
+
if (draw(g, w, h, now))
|
|
1464
|
+
raf = requestAnimationFrame(frame);
|
|
1465
|
+
};
|
|
1466
|
+
return {
|
|
1467
|
+
kick: () => {
|
|
1468
|
+
if (!raf && g)
|
|
1469
|
+
raf = requestAnimationFrame(frame);
|
|
1470
|
+
},
|
|
1471
|
+
stop: () => {
|
|
1472
|
+
cancelAnimationFrame(raf);
|
|
1473
|
+
raf = 0;
|
|
1474
|
+
c.remove();
|
|
1475
|
+
},
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
/** Pointer trail on `el`: keeps the last `life` ms of points and redraws them. */
|
|
1479
|
+
function trail(el, o, paint) {
|
|
1480
|
+
const pts = [];
|
|
1481
|
+
const oc = overlayCanvas((g, _w, _h, now) => {
|
|
1482
|
+
while (pts.length && now - pts[0].t > o.life)
|
|
1483
|
+
pts.shift();
|
|
1484
|
+
if (pts.length > 1)
|
|
1485
|
+
paint(g, pts, now);
|
|
1486
|
+
return pts.length > 0;
|
|
1487
|
+
});
|
|
1488
|
+
const move = (e) => {
|
|
1489
|
+
if (!touchOk(e, o))
|
|
1490
|
+
return;
|
|
1491
|
+
pts.push({ x: e.clientX, y: e.clientY, t: performance.now() });
|
|
1492
|
+
if (pts.length > 64)
|
|
1493
|
+
pts.shift();
|
|
1494
|
+
oc.kick();
|
|
1495
|
+
};
|
|
1496
|
+
el.addEventListener('pointermove', move, { passive: true });
|
|
1497
|
+
return () => {
|
|
1498
|
+
el.removeEventListener('pointermove', move);
|
|
1499
|
+
oc.stop();
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const CURSOR_FX = [
|
|
1503
|
+
{
|
|
1504
|
+
name: 'comet-trail',
|
|
1505
|
+
kind: 'cursor',
|
|
1506
|
+
description: 'A glowing comet tail follows the pointer over the element (persistent; `color`, `width`, `life`).',
|
|
1507
|
+
defaults: { color: '#7c5cff', width: 10, life: 320, touch: false },
|
|
1508
|
+
run: (el, o) => trail(el, o, (g, pts, now) => {
|
|
1509
|
+
const [r, gg, b] = hexRgb(o.color);
|
|
1510
|
+
g.lineCap = 'round';
|
|
1511
|
+
for (let i = 1; i < pts.length; i++) {
|
|
1512
|
+
const k = 1 - (now - pts[i].t) / o.life;
|
|
1513
|
+
g.strokeStyle = `rgba(${r},${gg},${b},${(k * 0.9).toFixed(3)})`;
|
|
1514
|
+
g.lineWidth = Math.max(0.5, o.width * k);
|
|
1515
|
+
g.beginPath();
|
|
1516
|
+
g.moveTo(pts[i - 1].x, pts[i - 1].y);
|
|
1517
|
+
g.lineTo(pts[i].x, pts[i].y);
|
|
1518
|
+
g.stroke();
|
|
1519
|
+
}
|
|
1520
|
+
}),
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: 'ribbon-trail',
|
|
1524
|
+
kind: 'cursor',
|
|
1525
|
+
description: 'A smooth rainbow ribbon flows behind the pointer (persistent; `width`, `life`).',
|
|
1526
|
+
defaults: { width: 18, life: 600, touch: false },
|
|
1527
|
+
run: (el, o) => trail(el, o, (g, pts, now) => {
|
|
1528
|
+
for (let i = 2; i < pts.length; i++) {
|
|
1529
|
+
const k = 1 - (now - pts[i].t) / o.life;
|
|
1530
|
+
const a = pts[i - 2];
|
|
1531
|
+
const m = pts[i - 1];
|
|
1532
|
+
const b = pts[i];
|
|
1533
|
+
g.strokeStyle = `hsla(${(pts[i].t / 6) % 360},90%,62%,${(k * 0.85).toFixed(3)})`;
|
|
1534
|
+
g.lineWidth = Math.max(0.5, o.width * k * Math.sin(Math.PI * Math.min(1, (i / pts.length) * 1.1)));
|
|
1535
|
+
g.lineCap = 'round';
|
|
1536
|
+
g.beginPath();
|
|
1537
|
+
g.moveTo((a.x + m.x) / 2, (a.y + m.y) / 2);
|
|
1538
|
+
g.quadraticCurveTo(m.x, m.y, (m.x + b.x) / 2, (m.y + b.y) / 2);
|
|
1539
|
+
g.stroke();
|
|
1540
|
+
}
|
|
1541
|
+
}),
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
name: 'sparkle-trail',
|
|
1545
|
+
kind: 'cursor',
|
|
1546
|
+
description: 'Little stars twinkle off the pointer as it moves (persistent; `colors`, `spacing` px between stars).',
|
|
1547
|
+
defaults: { colors: PALETTE, spacing: 14, size: 14, touch: false },
|
|
1548
|
+
run: (el, o, ctx) => {
|
|
1549
|
+
let lx = -1e4;
|
|
1550
|
+
let ly = -1e4;
|
|
1551
|
+
const move = (e) => {
|
|
1552
|
+
if (!touchOk(e, o) || Math.hypot(e.clientX - lx, e.clientY - ly) < o.spacing)
|
|
1553
|
+
return;
|
|
1554
|
+
lx = e.clientX;
|
|
1555
|
+
ly = e.clientY;
|
|
1556
|
+
const s = o.size * rand(0.6, 1.2);
|
|
1557
|
+
spawn(e.clientX - s / 2, e.clientY - s / 2, `font-size:${s}px;line-height:1;color:${o.colors[Math.floor(rand(0, o.colors.length))]}`, ctx, [{ transform: 'translate(0,0) scale(0) rotate(0deg)', opacity: 1 }, { transform: `translate(${rand(-14, 14)}px,${rand(4, 26)}px) scale(1) rotate(${rand(-90, 90)}deg)`, opacity: 0 }], { duration: rand(500, 800), easing: 'ease-out' }, '✦');
|
|
1558
|
+
};
|
|
1559
|
+
el.addEventListener('pointermove', move, { passive: true });
|
|
1560
|
+
return () => el.removeEventListener('pointermove', move);
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
name: 'magnetic-dots',
|
|
1565
|
+
kind: 'cursor',
|
|
1566
|
+
description: 'A grid of dots behind the content leans toward the pointer like iron filings to a magnet (persistent; `gap`, `radius`, `color`).',
|
|
1567
|
+
defaults: { gap: 22, radius: 120, color: '#7c5cff', background: 'transparent', touch: false },
|
|
1568
|
+
reduced: 'skip',
|
|
1569
|
+
run: (el, o, ctx) => {
|
|
1570
|
+
const p = { x: -1e4, y: -1e4 };
|
|
1571
|
+
const move = (e) => {
|
|
1572
|
+
if (!touchOk(e, o))
|
|
1573
|
+
return;
|
|
1574
|
+
const r = el.getBoundingClientRect();
|
|
1575
|
+
p.x = e.clientX - r.left;
|
|
1576
|
+
p.y = e.clientY - r.top;
|
|
1577
|
+
};
|
|
1578
|
+
const leave = () => ((p.x = -1e4), (p.y = -1e4));
|
|
1579
|
+
el.addEventListener('pointermove', move, { passive: true });
|
|
1580
|
+
el.addEventListener('pointerleave', leave);
|
|
1581
|
+
const off = canvasBackground(el, ctx, {
|
|
1582
|
+
draw: ({ ctx: g, w, h }) => {
|
|
1583
|
+
g.clearRect(0, 0, w, h);
|
|
1584
|
+
if (o.background !== 'transparent') {
|
|
1585
|
+
g.fillStyle = o.background;
|
|
1586
|
+
g.fillRect(0, 0, w, h);
|
|
1587
|
+
}
|
|
1588
|
+
g.fillStyle = o.color;
|
|
1589
|
+
for (let y = o.gap / 2; y < h; y += o.gap)
|
|
1590
|
+
for (let x = o.gap / 2; x < w; x += o.gap) {
|
|
1591
|
+
const dx = p.x - x;
|
|
1592
|
+
const dy = p.y - y;
|
|
1593
|
+
const d = Math.hypot(dx, dy);
|
|
1594
|
+
const k = d < o.radius ? (1 - d / o.radius) ** 2 : 0;
|
|
1595
|
+
const s = 1.5 + k * 3;
|
|
1596
|
+
g.globalAlpha = 0.35 + k * 0.65;
|
|
1597
|
+
g.fillRect(x + dx * k * 0.35 - s / 2, y + dy * k * 0.35 - s / 2, s, s);
|
|
1598
|
+
}
|
|
1599
|
+
g.globalAlpha = 1;
|
|
1600
|
+
},
|
|
1601
|
+
}, o);
|
|
1602
|
+
return () => {
|
|
1603
|
+
el.removeEventListener('pointermove', move);
|
|
1604
|
+
el.removeEventListener('pointerleave', leave);
|
|
1605
|
+
off();
|
|
1606
|
+
};
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
name: 'spotlight-cursor',
|
|
1611
|
+
kind: 'cursor',
|
|
1612
|
+
description: 'A soft light follows the pointer across the element, easing behind it (persistent; `color`, `size`).',
|
|
1613
|
+
defaults: { color: '#ffffff', size: 260, ease: 0.18, touch: false },
|
|
1614
|
+
run: (el, o) => {
|
|
1615
|
+
const [r, g, b] = hexRgb(o.color);
|
|
1616
|
+
const s = document.createElement('span');
|
|
1617
|
+
s.setAttribute('aria-hidden', 'true');
|
|
1618
|
+
s.style.cssText = `position:absolute;inset:0;pointer-events:none;border-radius:inherit;opacity:0;transition:opacity .25s;mix-blend-mode:soft-light;background:radial-gradient(circle ${o.size / 2}px at var(--sx,50%) var(--sy,50%),rgba(${r},${g},${b},.75),transparent)`;
|
|
1619
|
+
const restore = el.style.position;
|
|
1620
|
+
if (getComputedStyle(el).position === 'static')
|
|
1621
|
+
el.style.position = 'relative';
|
|
1622
|
+
el.appendChild(s);
|
|
1623
|
+
let tx = 0;
|
|
1624
|
+
let ty = 0;
|
|
1625
|
+
let x = 0;
|
|
1626
|
+
let y = 0;
|
|
1627
|
+
let raf = 0;
|
|
1628
|
+
const tick = () => {
|
|
1629
|
+
x += (tx - x) * o.ease;
|
|
1630
|
+
y += (ty - y) * o.ease;
|
|
1631
|
+
s.style.setProperty('--sx', `${x.toFixed(1)}px`);
|
|
1632
|
+
s.style.setProperty('--sy', `${y.toFixed(1)}px`);
|
|
1633
|
+
raf = Math.abs(tx - x) + Math.abs(ty - y) > 0.3 ? requestAnimationFrame(tick) : 0;
|
|
1634
|
+
};
|
|
1635
|
+
const move = (e) => {
|
|
1636
|
+
if (!touchOk(e, o))
|
|
1637
|
+
return;
|
|
1638
|
+
const rc = el.getBoundingClientRect();
|
|
1639
|
+
tx = e.clientX - rc.left;
|
|
1640
|
+
ty = e.clientY - rc.top;
|
|
1641
|
+
if (s.style.opacity !== '1')
|
|
1642
|
+
((x = tx), (y = ty), (s.style.opacity = '1'));
|
|
1643
|
+
if (!raf)
|
|
1644
|
+
raf = requestAnimationFrame(tick);
|
|
1645
|
+
};
|
|
1646
|
+
const leave = () => (s.style.opacity = '0');
|
|
1647
|
+
el.addEventListener('pointermove', move, { passive: true });
|
|
1648
|
+
el.addEventListener('pointerleave', leave);
|
|
1649
|
+
return () => {
|
|
1650
|
+
cancelAnimationFrame(raf);
|
|
1651
|
+
el.removeEventListener('pointermove', move);
|
|
1652
|
+
el.removeEventListener('pointerleave', leave);
|
|
1653
|
+
s.remove();
|
|
1654
|
+
el.style.position = restore;
|
|
1655
|
+
};
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
];
|
|
1659
|
+
// --- gestures → effects ------------------------------------------------------
|
|
1660
|
+
const GESTURES = ['fling', 'twist', 'long-press'];
|
|
1661
|
+
/** Release velocity (px/ms) from recent pointer samples: uses the last `window` ms (pure). */
|
|
1662
|
+
function flingVelocity(pts, window = 90) {
|
|
1663
|
+
if (pts.length < 2)
|
|
1664
|
+
return { vx: 0, vy: 0, speed: 0 };
|
|
1665
|
+
const last = pts[pts.length - 1];
|
|
1666
|
+
let first = pts[0];
|
|
1667
|
+
for (let i = pts.length - 2; i >= 0; i--) {
|
|
1668
|
+
first = pts[i];
|
|
1669
|
+
if (last.t - pts[i].t >= window)
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
const dt = Math.max(1, last.t - first.t);
|
|
1673
|
+
const vx = (last.x - first.x) / dt;
|
|
1674
|
+
const vy = (last.y - first.y) / dt;
|
|
1675
|
+
return { vx, vy, speed: Math.hypot(vx, vy) };
|
|
1676
|
+
}
|
|
1677
|
+
/** Signed smallest difference between two angles in degrees, in (-180, 180] (pure). */
|
|
1678
|
+
function angleDelta(a, b) {
|
|
1679
|
+
let d = (b - a) % 360;
|
|
1680
|
+
if (d > 180)
|
|
1681
|
+
d -= 360;
|
|
1682
|
+
if (d <= -180)
|
|
1683
|
+
d += 360;
|
|
1684
|
+
return d;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Fire `effect` (a registered effect name, or a callback) when `gesture`
|
|
1688
|
+
* happens on `el`. Returns an unbind.
|
|
1689
|
+
*/
|
|
1690
|
+
function bindGesture(el, gesture, effect, o = {}) {
|
|
1691
|
+
const { velocity = 0.8, angle = 30, duration = 650, tolerance = 10, effectOptions = {} } = o;
|
|
1692
|
+
const pts = new Map();
|
|
1693
|
+
let path = [];
|
|
1694
|
+
let base = null;
|
|
1695
|
+
let press = null;
|
|
1696
|
+
const fire = (d, e) => {
|
|
1697
|
+
el.dispatchEvent(new CustomEvent('usa-gesture', { detail: d, bubbles: true }));
|
|
1698
|
+
if (typeof effect === 'function')
|
|
1699
|
+
effect(d, e);
|
|
1700
|
+
else
|
|
1701
|
+
playEffect(el, effect, effectOptions, e).catch(() => undefined);
|
|
1702
|
+
};
|
|
1703
|
+
const twistAngle = () => {
|
|
1704
|
+
const [a, b] = [...pts.values()];
|
|
1705
|
+
return (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
|
|
1706
|
+
};
|
|
1707
|
+
const charge = (k) => {
|
|
1708
|
+
el.style.setProperty('--usa-charge', k.toFixed(3));
|
|
1709
|
+
el.toggleAttribute('data-charging', k > 0 && k < 1);
|
|
1710
|
+
};
|
|
1711
|
+
const endPress = () => {
|
|
1712
|
+
if (!press)
|
|
1713
|
+
return;
|
|
1714
|
+
cancelAnimationFrame(press.raf);
|
|
1715
|
+
press = null;
|
|
1716
|
+
charge(0);
|
|
1717
|
+
};
|
|
1718
|
+
const down = (e) => {
|
|
1719
|
+
pts.set(e.pointerId ?? 1, { x: e.clientX, y: e.clientY });
|
|
1720
|
+
if (gesture === 'fling')
|
|
1721
|
+
path = [{ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() }];
|
|
1722
|
+
if (gesture === 'twist' && pts.size === 2)
|
|
1723
|
+
base = twistAngle();
|
|
1724
|
+
if (gesture === 'long-press' && pts.size === 1) {
|
|
1725
|
+
press = { x: e.clientX, y: e.clientY, t0: performance.now(), raf: 0 };
|
|
1726
|
+
const step = (now) => {
|
|
1727
|
+
if (!press)
|
|
1728
|
+
return;
|
|
1729
|
+
const k = Math.min(1, (now - press.t0) / duration);
|
|
1730
|
+
charge(k);
|
|
1731
|
+
if (k >= 1) {
|
|
1732
|
+
endPress();
|
|
1733
|
+
el.style.setProperty('--usa-charge', '1');
|
|
1734
|
+
fire({ gesture, charge: 1 }, e);
|
|
1735
|
+
}
|
|
1736
|
+
else
|
|
1737
|
+
press.raf = requestAnimationFrame(step);
|
|
1738
|
+
};
|
|
1739
|
+
press.raf = requestAnimationFrame(step);
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
const move = (e) => {
|
|
1743
|
+
if (!pts.has(e.pointerId ?? 1))
|
|
1744
|
+
return;
|
|
1745
|
+
pts.set(e.pointerId ?? 1, { x: e.clientX, y: e.clientY });
|
|
1746
|
+
if (gesture === 'fling') {
|
|
1747
|
+
path.push({ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() });
|
|
1748
|
+
if (path.length > 20)
|
|
1749
|
+
path.shift();
|
|
1750
|
+
}
|
|
1751
|
+
else if (gesture === 'twist' && pts.size >= 2 && base !== null) {
|
|
1752
|
+
const now = twistAngle();
|
|
1753
|
+
const d = angleDelta(base, now);
|
|
1754
|
+
if (Math.abs(d) >= angle) {
|
|
1755
|
+
base = now;
|
|
1756
|
+
fire({ gesture, angle: d, direction: d > 0 ? 'cw' : 'ccw' }, e);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
else if (gesture === 'long-press' && press && Math.hypot(e.clientX - press.x, e.clientY - press.y) > tolerance)
|
|
1760
|
+
endPress();
|
|
1761
|
+
};
|
|
1762
|
+
const up = (e) => {
|
|
1763
|
+
pts.delete(e.pointerId ?? 1);
|
|
1764
|
+
if (gesture === 'fling' && path.length) {
|
|
1765
|
+
path.push({ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() });
|
|
1766
|
+
const v = flingVelocity(path);
|
|
1767
|
+
path = [];
|
|
1768
|
+
if (v.speed >= velocity) {
|
|
1769
|
+
const direction = Math.abs(v.vx) > Math.abs(v.vy) ? (v.vx > 0 ? 'right' : 'left') : v.vy > 0 ? 'down' : 'up';
|
|
1770
|
+
fire({ gesture, ...v, direction }, e);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (pts.size < 2)
|
|
1774
|
+
base = null;
|
|
1775
|
+
endPress();
|
|
1776
|
+
};
|
|
1777
|
+
el.addEventListener('pointerdown', down);
|
|
1778
|
+
el.addEventListener('pointermove', move);
|
|
1779
|
+
el.addEventListener('pointerup', up);
|
|
1780
|
+
el.addEventListener('pointercancel', up);
|
|
1781
|
+
if (gesture === 'twist' && !el.style.touchAction)
|
|
1782
|
+
el.style.touchAction = 'none';
|
|
1783
|
+
return () => {
|
|
1784
|
+
endPress();
|
|
1785
|
+
el.removeEventListener('pointerdown', down);
|
|
1786
|
+
el.removeEventListener('pointermove', move);
|
|
1787
|
+
el.removeEventListener('pointerup', up);
|
|
1788
|
+
el.removeEventListener('pointercancel', up);
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* `<usa-gesture-fx gesture="fling | twist | long-press" effect="tada"
|
|
1793
|
+
* options='{"…"}' velocity angle duration>` — plays `effect` on its first
|
|
1794
|
+
* child (or itself with `self`) when the gesture happens.
|
|
1795
|
+
*/
|
|
1796
|
+
function defineGestureFx(tag = 'usa-gesture-fx') {
|
|
1797
|
+
return defineElement(tag, (Base) => class UsaGestureFx extends Base {
|
|
1798
|
+
static get observedAttributes() {
|
|
1799
|
+
return ['gesture', 'effect', 'options'];
|
|
1800
|
+
}
|
|
1801
|
+
get gesture() {
|
|
1802
|
+
const g = this.str('gesture', 'fling');
|
|
1803
|
+
return GESTURES.includes(g) ? g : 'fling';
|
|
1804
|
+
}
|
|
1805
|
+
mount() {
|
|
1806
|
+
let effectOptions = {};
|
|
1807
|
+
try {
|
|
1808
|
+
effectOptions = JSON.parse(this.str('options', '{}')) || {};
|
|
1809
|
+
}
|
|
1810
|
+
catch {
|
|
1811
|
+
/* ignore bad JSON */
|
|
1812
|
+
}
|
|
1813
|
+
const target = this.flag('self') ? this : this.firstElementChild || this;
|
|
1814
|
+
const effect = this.str('effect', 'pulse');
|
|
1815
|
+
this.onCleanup(bindGesture(this, this.gesture, (_d, e) => void playEffect(target, effect, effectOptions, e).catch(() => undefined), {
|
|
1816
|
+
velocity: this.num('velocity', 0.8),
|
|
1817
|
+
angle: this.num('angle', 30),
|
|
1818
|
+
duration: this.num('duration', 650),
|
|
1819
|
+
}));
|
|
1820
|
+
}
|
|
1821
|
+
}, { id: 'usa-gesture-fx', text: 'usa-gesture-fx{display:inline-block;touch-action:none;user-select:none}' });
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
const saved = new WeakMap();
|
|
1825
|
+
/** Toggle `aria-pressed` (or set it) and return the new state. */
|
|
1826
|
+
function togglePressed(el, force) {
|
|
1827
|
+
const on = force ?? el.getAttribute('aria-pressed') !== 'true';
|
|
1828
|
+
el.setAttribute('aria-pressed', String(on));
|
|
1829
|
+
return on;
|
|
1830
|
+
}
|
|
1831
|
+
/** Swap `el`'s label for `ms` (polite live region), then restore it. */
|
|
1832
|
+
function swapLabel(el, text, ms) {
|
|
1833
|
+
if (!saved.has(el))
|
|
1834
|
+
saved.set(el, el.innerHTML);
|
|
1835
|
+
el.setAttribute('aria-live', 'polite');
|
|
1836
|
+
el.textContent = text;
|
|
1837
|
+
return new Promise((r) => setTimeout(() => {
|
|
1838
|
+
el.innerHTML = saved.get(el);
|
|
1839
|
+
saved.delete(el);
|
|
1840
|
+
el.removeAttribute('aria-live');
|
|
1841
|
+
r();
|
|
1842
|
+
}, ms));
|
|
1843
|
+
}
|
|
1844
|
+
/** Add `delta` to the number in `[data-count]` (or `el`), keeping it in `data-count`. Returns the new value. */
|
|
1845
|
+
function bumpCount(el, delta, ctx) {
|
|
1846
|
+
const t = el.querySelector('[data-count]') || el;
|
|
1847
|
+
const n = (Number(t.dataset.count ?? (t.textContent || '').replace(/[^\d.-]/g, '')) || 0) + delta;
|
|
1848
|
+
t.dataset.count = String(n);
|
|
1849
|
+
t.textContent = String(n);
|
|
1850
|
+
ctx?.animate(t, [{ transform: `translateY(${delta > 0 ? 60 : -60}%)`, opacity: 0 }, { transform: 'none', opacity: 1 }], { duration: 260, easing: 'cubic-bezier(.2,1.4,.4,1)' });
|
|
1851
|
+
return n;
|
|
1852
|
+
}
|
|
1853
|
+
const pop = (el, ctx, s = 1.25, d = 380) => ctx.animate(el, [{ transform: 'scale(1)' }, { transform: `scale(${s})`, offset: 0.4 }, { transform: 'scale(1)' }], { duration: d, easing: 'cubic-bezier(.2,1.4,.4,1)' });
|
|
1854
|
+
/** A few glyphs flying out of the pointer / centre. */
|
|
1855
|
+
function burst(el, ctx, glyph, color, n = 6, rise = false) {
|
|
1856
|
+
const { x, y } = origin(el, ctx);
|
|
1857
|
+
return all(Array.from({ length: n }, (_, i) => {
|
|
1858
|
+
const a = rise ? -Math.PI / 2 + rand(-0.6, 0.6) : (i / n) * Math.PI * 2;
|
|
1859
|
+
const d = rand(28, 56);
|
|
1860
|
+
return spawn(x - 7, y - 7, `font-size:14px;line-height:1;color:${color}`, ctx, [{ transform: 'translate(0,0) scale(.4)', opacity: 1 }, { transform: `translate(${Math.cos(a) * d}px,${Math.sin(a) * d}px) scale(1)`, opacity: 0 }], { duration: rand(500, 750), easing: 'cubic-bezier(.2,.8,.3,1)' }, glyph);
|
|
1861
|
+
}));
|
|
1862
|
+
}
|
|
1863
|
+
const click = (name, description, defaults, run) => ({ name, kind: 'click', description, defaults, run });
|
|
1864
|
+
const attn = (name, description, defaults, run) => ({ name, kind: 'attention', description, defaults, run });
|
|
1865
|
+
const MICRO_FX = [
|
|
1866
|
+
click('copy-success', 'Copies `text` (or `data-copy` / the target of `for`) to the clipboard and swaps the label to "Copied ✓".', { text: '', label: 'Copied ✓', ms: 1500 }, (el, o, ctx) => {
|
|
1867
|
+
const src = el.getAttribute('for') ? document.getElementById(el.getAttribute('for')) : null;
|
|
1868
|
+
const text = o.text || el.dataset.copy || src?.value || src?.textContent || '';
|
|
1869
|
+
navigator.clipboard?.writeText(text).catch(() => undefined);
|
|
1870
|
+
pop(el, ctx, 1.08);
|
|
1871
|
+
return swapLabel(el, o.label, o.ms);
|
|
1872
|
+
}),
|
|
1873
|
+
click('toggle-morph', 'Toggles `aria-pressed` with a squash-and-stretch morph.', {}, (el, _o, ctx) => {
|
|
1874
|
+
const on = togglePressed(el);
|
|
1875
|
+
return ctx.animate(el, [{ transform: 'scale(1,1)' }, { transform: `scale(${on ? 1.15 : 0.85},${on ? 0.85 : 1.15})`, offset: 0.35 }, { transform: 'scale(1,1)' }], { duration: 360, easing: 'ease-out' });
|
|
1876
|
+
}),
|
|
1877
|
+
click('password-reveal', 'Shows / hides the password input (`for` id, or the previous input) with an eye blink.', { show: 'Hide password', hide: 'Show password' }, (el, o, ctx) => {
|
|
1878
|
+
const input = (el.getAttribute('for') ? document.getElementById(el.getAttribute('for')) : el.previousElementSibling);
|
|
1879
|
+
if (!input || !('type' in input))
|
|
1880
|
+
return;
|
|
1881
|
+
const show = input.type === 'password';
|
|
1882
|
+
input.type = show ? 'text' : 'password';
|
|
1883
|
+
togglePressed(el, show);
|
|
1884
|
+
el.setAttribute('aria-label', show ? o.show : o.hide);
|
|
1885
|
+
return ctx.animate(el, [{ transform: 'scaleY(1)' }, { transform: 'scaleY(.1)', offset: 0.5 }, { transform: 'scaleY(1)' }], { duration: 240, easing: 'ease-in-out' });
|
|
1886
|
+
}),
|
|
1887
|
+
click('favorite-star', 'Toggles a favourite: the star pops and throws little stars (`color`).', { color: '#facc15' }, (el, o, ctx) => {
|
|
1888
|
+
const on = togglePressed(el);
|
|
1889
|
+
return Promise.all([pop(el, ctx, on ? 1.35 : 0.85)?.finished, on ? burst(el, ctx, '★', o.color) : null]);
|
|
1890
|
+
}),
|
|
1891
|
+
click('like-heart', 'Toggles a like: the heart beats and hearts float up (`color`).', { color: '#ff5c8a' }, (el, o, ctx) => {
|
|
1892
|
+
const on = togglePressed(el);
|
|
1893
|
+
return Promise.all([pop(el, ctx, on ? 1.3 : 0.9)?.finished, on ? burst(el, ctx, '♥', o.color, 5, true) : null]);
|
|
1894
|
+
}),
|
|
1895
|
+
click('bookmark-flip', 'Toggles a bookmark with a 3D flip.', {}, (el, _o, ctx) => {
|
|
1896
|
+
togglePressed(el);
|
|
1897
|
+
return ctx.animate(el, [{ transform: 'perspective(400px) rotateY(0)' }, { transform: 'perspective(400px) rotateY(180deg)' }, { transform: 'perspective(400px) rotateY(360deg)' }], { duration: 520, easing: 'ease-in-out' });
|
|
1898
|
+
}),
|
|
1899
|
+
click('download-progress', 'Fills a progress bar over `duration` ms, then shows "Done ✓" (`aria-busy` while running; fires `usa-done`).', { duration: 1600, label: 'Done ✓', color: '#34d399' }, (el, o, ctx) => {
|
|
1900
|
+
if (el.getAttribute('aria-busy') === 'true')
|
|
1901
|
+
return;
|
|
1902
|
+
el.setAttribute('aria-busy', 'true');
|
|
1903
|
+
const [bar, remove] = overlay(el, `background:${o.color}55;transform-origin:left;transform:scaleX(0)`);
|
|
1904
|
+
const a = ctx.animate(bar, [{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], { duration: o.duration, easing: 'linear', fill: 'forwards' });
|
|
1905
|
+
const done = a ? a.finished.catch(() => undefined) : Promise.resolve();
|
|
1906
|
+
return done.then(() => {
|
|
1907
|
+
remove();
|
|
1908
|
+
el.removeAttribute('aria-busy');
|
|
1909
|
+
el.dispatchEvent(new CustomEvent('usa-done', { bubbles: true }));
|
|
1910
|
+
return swapLabel(el, o.label, 1400);
|
|
1911
|
+
});
|
|
1912
|
+
}),
|
|
1913
|
+
click('submit-loading', 'Shows "Sending…" with pulsing dots for `duration` ms, then "Sent ✓" (`aria-busy`, `usa-done`).', { duration: 1200, loading: 'Sending…', label: 'Sent ✓' }, (el, o, ctx) => {
|
|
1914
|
+
if (el.getAttribute('aria-busy') === 'true')
|
|
1915
|
+
return;
|
|
1916
|
+
el.setAttribute('aria-busy', 'true');
|
|
1917
|
+
const t = swapLabel(el, o.loading, o.duration);
|
|
1918
|
+
const a = ctx.animate(el, [{ opacity: 1 }, { opacity: 0.55 }, { opacity: 1 }], { duration: 600, iterations: Math.max(1, Math.round(o.duration / 600)) });
|
|
1919
|
+
return t.then(() => {
|
|
1920
|
+
a?.cancel();
|
|
1921
|
+
el.removeAttribute('aria-busy');
|
|
1922
|
+
el.dispatchEvent(new CustomEvent('usa-done', { bubbles: true }));
|
|
1923
|
+
return swapLabel(el, o.label, 1400);
|
|
1924
|
+
});
|
|
1925
|
+
}),
|
|
1926
|
+
click('send-plane', 'A paper plane ✈ takes off from the button.', { color: '#22d3ee' }, (el, o, ctx) => {
|
|
1927
|
+
const r = el.getBoundingClientRect();
|
|
1928
|
+
pop(el, ctx, 0.92, 200);
|
|
1929
|
+
return spawn(r.left + r.width / 2 - 8, r.top + r.height / 2 - 8, `font-size:16px;color:${o.color}`, ctx, [{ transform: 'translate(0,0) rotate(0)', opacity: 1 }, { transform: 'translate(40px,-10px) rotate(-10deg)', opacity: 1, offset: 0.4 }, { transform: 'translate(160px,-90px) rotate(-25deg)', opacity: 0 }], { duration: 800, easing: 'ease-in' }, '✈')?.finished;
|
|
1930
|
+
}),
|
|
1931
|
+
click('add-to-cart', 'Bumps `[data-count]` by one and floats a "+1" (`text`).', { text: '+1', color: '#34d399' }, (el, o, ctx) => {
|
|
1932
|
+
bumpCount(el, 1, ctx);
|
|
1933
|
+
const { x, y } = origin(el, ctx);
|
|
1934
|
+
return spawn(x - 10, y - 10, `font:700 14px system-ui;color:${o.color}`, ctx, [{ transform: 'translateY(0)', opacity: 1 }, { transform: 'translateY(-40px)', opacity: 0 }], { duration: 700, easing: 'ease-out' }, o.text)?.finished;
|
|
1935
|
+
}),
|
|
1936
|
+
click('counter-bump', 'Adds `step` to `[data-count]` with a rolling number.', { step: 1 }, (el, o, ctx) => void bumpCount(el, Number(o.step) || 1, ctx)),
|
|
1937
|
+
click('upvote', 'Toggles an upvote: the arrow nudges up and `[data-count]` ±1.', {}, (el, _o, ctx) => {
|
|
1938
|
+
const on = togglePressed(el);
|
|
1939
|
+
bumpCount(el, on ? 1 : -1, ctx);
|
|
1940
|
+
return ctx.animate(el, [{ transform: 'translateY(0)' }, { transform: `translateY(${on ? -6 : 4}px)`, offset: 0.4 }, { transform: 'translateY(0)' }], { duration: 320, easing: 'cubic-bezier(.2,1.4,.4,1)' });
|
|
1941
|
+
}),
|
|
1942
|
+
click('clap', 'Counts claps in `[data-count]` with a 👏 burst on every press.', {}, (el, _o, ctx) => {
|
|
1943
|
+
bumpCount(el, 1, ctx);
|
|
1944
|
+
return Promise.all([pop(el, ctx, 1.15, 240)?.finished, burst(el, ctx, '👏', 'inherit', 3, true)]);
|
|
1945
|
+
}),
|
|
1946
|
+
click('emoji-react', 'Pops one `emoji` up from the pointer (reaction button).', { emoji: '🎉' }, (el, o, ctx) => burst(el, ctx, o.emoji, 'inherit', 1, true)),
|
|
1947
|
+
click('refresh-spin', 'Spins the icon one turn (`turns`).', { turns: 1 }, (el, o, ctx) => ctx.animate(el, [{ transform: 'rotate(0)' }, { transform: `rotate(${360 * o.turns}deg)` }], { duration: 600 * o.turns, easing: 'cubic-bezier(.4,0,.2,1)' })),
|
|
1948
|
+
click('trash-shake', 'Shakes, then drops and fades (`remove: true` removes the element afterwards).', { remove: false }, (el, o, ctx) => {
|
|
1949
|
+
const a = ctx.animate(el, [{ transform: 'none', opacity: 1 }, { transform: 'rotate(-6deg)', offset: 0.15 }, { transform: 'rotate(6deg)', offset: 0.3 }, { transform: 'rotate(0)', offset: 0.45, opacity: 1 }, { transform: 'translateY(30px) scale(.8)', opacity: 0 }], { duration: 650, easing: 'ease-in', fill: 'forwards' });
|
|
1950
|
+
const end = () => (o.remove ? el.remove() : a?.cancel());
|
|
1951
|
+
return a ? a.finished.then(end, end) : void end();
|
|
1952
|
+
}),
|
|
1953
|
+
click('check-toggle', 'Toggles a check mark (`aria-checked` for role=checkbox, else `aria-pressed`) that scales in.', {}, (el, _o, ctx) => {
|
|
1954
|
+
const attr = el.getAttribute('role') === 'checkbox' ? 'aria-checked' : 'aria-pressed';
|
|
1955
|
+
const on = el.getAttribute(attr) !== 'true';
|
|
1956
|
+
el.setAttribute(attr, String(on));
|
|
1957
|
+
return ctx.animate(el, on ? [{ transform: 'scale(.6)' }, { transform: 'scale(1.15)', offset: 0.6 }, { transform: 'scale(1)' }] : [{ transform: 'scale(1)' }, { transform: 'scale(.85)' }, { transform: 'scale(1)' }], { duration: 280, easing: 'ease-out' });
|
|
1958
|
+
}),
|
|
1959
|
+
attn('input-shake', 'Shakes an invalid field side to side and sets `aria-invalid` (`color` outline flash).', { color: '#ff5c8a' }, (el, o, ctx) => {
|
|
1960
|
+
el.setAttribute('aria-invalid', 'true');
|
|
1961
|
+
const prev = el.style.outline;
|
|
1962
|
+
el.style.outline = `2px solid ${o.color}`;
|
|
1963
|
+
setTimeout(() => (el.style.outline = prev), 900);
|
|
1964
|
+
return ctx.animate(el, [0, -8, 8, -6, 6, -3, 0].map((x) => ({ transform: `translateX(${x}px)` })), { duration: 420, easing: 'ease-out' });
|
|
1965
|
+
}),
|
|
1966
|
+
attn('error-flash', 'Flashes the element red once (no more than one flash per call).', { color: '#ff5c8a' }, (el, o, ctx) => ctx.animate(el, [{ boxShadow: `0 0 0 0 ${o.color}00` }, { boxShadow: `0 0 0 4px ${o.color}`, offset: 0.3 }, { boxShadow: `0 0 0 0 ${o.color}00` }], { duration: 700 })),
|
|
1967
|
+
attn('success-check', 'A green ✓ badge pops over the element and fades.', { color: '#34d399' }, (el, o, ctx) => {
|
|
1968
|
+
const [b, remove] = overlay(el, `display:grid;place-items:center;font:700 22px system-ui;color:#fff;background:${o.color}d0`);
|
|
1969
|
+
b.textContent = '✓';
|
|
1970
|
+
const a = ctx.animate(b, [{ opacity: 0, transform: 'scale(.6)' }, { opacity: 1, transform: 'scale(1)', offset: 0.3 }, { opacity: 1, offset: 0.75 }, { opacity: 0 }], { duration: 1100, easing: 'ease-out' });
|
|
1971
|
+
return a ? a.finished.then(remove, remove) : void setTimeout(remove, 900);
|
|
1972
|
+
}),
|
|
1973
|
+
attn('nudge-hint', 'A small sideways nudge that says "try me" (`distance`).', { distance: 6 }, (el, o, ctx) => ctx.animate(el, [0, o.distance, 0, o.distance / 2, 0].map((x) => ({ transform: `translateX(${x}px)` })), { duration: 700, easing: 'ease-in-out' })),
|
|
1974
|
+
attn('focus-pulse', 'A ring pulses around the element to draw focus to it (`color`).', { color: '#7c5cff' }, (el, o, ctx) => ctx.animate(el, [{ boxShadow: `0 0 0 0 ${o.color}aa` }, { boxShadow: `0 0 0 12px ${o.color}00` }], { duration: 900, iterations: 2, easing: 'ease-out' })),
|
|
1975
|
+
attn('notify-badge', 'Bumps the badge count (`[data-count]`, `step`) with a pop — for new notifications.', { step: 1 }, (el, o, ctx) => {
|
|
1976
|
+
bumpCount(el, Number(o.step) || 1, ctx);
|
|
1977
|
+
return pop(el, ctx, 1.3, 320);
|
|
1978
|
+
}),
|
|
1979
|
+
];
|
|
1980
|
+
|
|
1981
|
+
const THEME_ROLES = ['enter', 'hover', 'click', 'attention', 'background'];
|
|
1982
|
+
const P = (enter, hover, click, attention, background) => ({
|
|
1983
|
+
enter: { effect: enter },
|
|
1984
|
+
hover: { effect: hover },
|
|
1985
|
+
click: { effect: click },
|
|
1986
|
+
attention: { effect: attention },
|
|
1987
|
+
background: { effect: background },
|
|
1988
|
+
});
|
|
1989
|
+
const THEMES = {
|
|
1990
|
+
neon: {
|
|
1991
|
+
name: 'neon',
|
|
1992
|
+
vars: { bg: '#07070c', fg: '#e8e8ff', accent: '#22d3ee', 'accent-2': '#ff2bd6', surface: '#11111c', border: '1px solid #22d3ee', radius: '10px', shadow: '0 0 18px #22d3ee66, inset 0 0 12px #ff2bd622', font: 'ui-monospace, SFMono-Regular, Menlo, monospace' },
|
|
1993
|
+
motion: { duration: { fast: 120, normal: 220 }, easing: { standard: 'cubic-bezier(0.2, 0, 0, 1)' } },
|
|
1994
|
+
presets: P('fade-up', 'neon-flicker', 'shockwave', 'neon-flicker', 'starfield'),
|
|
1995
|
+
},
|
|
1996
|
+
paper: {
|
|
1997
|
+
name: 'paper',
|
|
1998
|
+
vars: { bg: '#f6f1e7', fg: '#2b2721', accent: '#c2410c', 'accent-2': '#0f766e', surface: '#fffdf8', border: '1px solid #e3dccd', radius: '4px', shadow: '0 1px 0 #0000000d, 0 10px 24px -14px #00000040', font: 'Georgia, "Times New Roman", serif' },
|
|
1999
|
+
motion: { duration: { fast: 200, normal: 380, slow: 700 }, easing: { standard: 'cubic-bezier(0, 0, 0, 1)' } },
|
|
2000
|
+
presets: P('paper-fold', 'wiggle', 'ink-splash', 'nudge-hint', 'contours'),
|
|
2001
|
+
},
|
|
2002
|
+
glass: {
|
|
2003
|
+
name: 'glass',
|
|
2004
|
+
vars: { bg: 'linear-gradient(135deg, #1e1b4b, #0e7490)', fg: '#f8fafc', accent: '#a5f3fc', 'accent-2': '#c4b5fd', surface: '#ffffff1f', border: '1px solid #ffffff40', radius: '18px', shadow: '0 8px 32px #0000003d', font: 'system-ui, -apple-system, "Segoe UI", sans-serif' },
|
|
2005
|
+
motion: { duration: { normal: 320 }, easing: { standard: 'cubic-bezier(0.22, 1, 0.36, 1)' } },
|
|
2006
|
+
presets: P('blur', 'glass-shine', 'ripple', 'focus-pulse', 'mesh-gradient'),
|
|
2007
|
+
},
|
|
2008
|
+
retro: {
|
|
2009
|
+
name: 'retro',
|
|
2010
|
+
vars: { bg: '#1d1135', fg: '#ffe9a8', accent: '#ff6b35', 'accent-2': '#2ec4b6', surface: '#2a1b4d', border: '3px solid #ffe9a8', radius: '0', shadow: '4px 4px 0 #ff6b35', font: '"Courier New", ui-monospace, monospace' },
|
|
2011
|
+
motion: { duration: { normal: 300 }, easing: { standard: 'steps(6, end)' } },
|
|
2012
|
+
presets: P('clip-up', 'rubber-band', 'star-burst', 'tada', 'retro-scanlines'),
|
|
2013
|
+
},
|
|
2014
|
+
brutalist: {
|
|
2015
|
+
name: 'brutalist',
|
|
2016
|
+
vars: { bg: '#ffffff', fg: '#000000', accent: '#ff3b00', 'accent-2': '#0047ff', surface: '#fff200', border: '3px solid #000000', radius: '0', shadow: '6px 6px 0 #000000', font: '"Arial Black", Arial, sans-serif' },
|
|
2017
|
+
motion: { duration: { fast: 80, normal: 160 }, easing: { standard: 'linear' } },
|
|
2018
|
+
presets: P('drop-bounce', 'jelly', 'brutal-shift', 'shake', 'voronoi'),
|
|
2019
|
+
},
|
|
2020
|
+
};
|
|
2021
|
+
const THEME_NAMES = Object.keys(THEMES);
|
|
2022
|
+
const BASE_CSS = `[data-usa-theme]{background:var(--usa-theme-bg);color:var(--usa-theme-fg);font-family:var(--usa-theme-font)}
|
|
2023
|
+
[data-usa-theme] .usa-surface{background:var(--usa-theme-surface);border:var(--usa-theme-border);border-radius:var(--usa-theme-radius);box-shadow:var(--usa-theme-shadow)}
|
|
2024
|
+
[data-usa-theme] .usa-accent{color:var(--usa-theme-accent)}
|
|
2025
|
+
[data-usa-theme=glass] .usa-surface{-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}`;
|
|
2026
|
+
const pack = (t) => {
|
|
2027
|
+
const p = typeof t === 'string' ? THEMES[t] : t;
|
|
2028
|
+
if (!p)
|
|
2029
|
+
throw new Error(`[use-scroll-animate] unknown theme "${String(t)}" — ${THEME_NAMES.join(', ')}`);
|
|
2030
|
+
return p;
|
|
2031
|
+
};
|
|
2032
|
+
/** The CSS custom properties of a theme (design + motion tokens). */
|
|
2033
|
+
function themeVars(t) {
|
|
2034
|
+
const p = pack(t);
|
|
2035
|
+
const out = {};
|
|
2036
|
+
for (const [k, v] of Object.entries(p.vars))
|
|
2037
|
+
out[`--usa-theme-${k}`] = v;
|
|
2038
|
+
return { ...out, ...motionTokensToVars(mergeMotionTokens(p.motion)) };
|
|
2039
|
+
}
|
|
2040
|
+
/** A theme as a CSS rule (`selector` default `[data-usa-theme=<name>]`) — for SSR / static CSS. */
|
|
2041
|
+
function themeCss(t, selector) {
|
|
2042
|
+
const p = pack(t);
|
|
2043
|
+
return `${selector || `[data-usa-theme=${p.name}]`}{${Object.entries(themeVars(p)).map(([k, v]) => `${k}:${v}`).join(';')}}`;
|
|
2044
|
+
}
|
|
2045
|
+
/** Apply a theme to `root` (default `<html>`). Returns an undo. */
|
|
2046
|
+
function applyTheme(t, root) {
|
|
2047
|
+
const p = pack(t);
|
|
2048
|
+
const el = root || document.documentElement;
|
|
2049
|
+
adoptStyles('usa-theme', BASE_CSS);
|
|
2050
|
+
const prevAttr = el.getAttribute('data-usa-theme');
|
|
2051
|
+
el.setAttribute('data-usa-theme', p.name);
|
|
2052
|
+
const vars = Object.entries(p.vars).map(([k, v]) => [`--usa-theme-${k}`, v]);
|
|
2053
|
+
const old = vars.map(([k]) => [k, el.style.getPropertyValue(k)]);
|
|
2054
|
+
for (const [k, v] of vars)
|
|
2055
|
+
el.style.setProperty(k, v);
|
|
2056
|
+
let undoMotion;
|
|
2057
|
+
if (el === document.documentElement)
|
|
2058
|
+
undoMotion = applyMotionTokens(p.motion, el);
|
|
2059
|
+
else {
|
|
2060
|
+
const mv = Object.entries(motionTokensToVars(mergeMotionTokens(p.motion)));
|
|
2061
|
+
const mold = mv.map(([k]) => [k, el.style.getPropertyValue(k)]);
|
|
2062
|
+
for (const [k, v] of mv)
|
|
2063
|
+
el.style.setProperty(k, v);
|
|
2064
|
+
undoMotion = () => mold.forEach(([k, v]) => (v ? el.style.setProperty(k, v) : el.style.removeProperty(k)));
|
|
2065
|
+
}
|
|
2066
|
+
return () => {
|
|
2067
|
+
undoMotion();
|
|
2068
|
+
old.forEach(([k, v]) => (v ? el.style.setProperty(k, v) : el.style.removeProperty(k)));
|
|
2069
|
+
if (prevAttr === null)
|
|
2070
|
+
el.removeAttribute('data-usa-theme');
|
|
2071
|
+
else
|
|
2072
|
+
el.setAttribute('data-usa-theme', prevAttr);
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
/** The effect preset of a theme for a role. */
|
|
2076
|
+
function themePreset(t, role) {
|
|
2077
|
+
return pack(t).presets[role];
|
|
2078
|
+
}
|
|
2079
|
+
/** Play the preset for `role` of the theme on the closest `[data-usa-theme]` (or `theme`). */
|
|
2080
|
+
function playThemeEffect(el, role, theme) {
|
|
2081
|
+
const name = theme || el.closest('[data-usa-theme]')?.getAttribute('data-usa-theme') || 'neon';
|
|
2082
|
+
const pr = themePreset(name, role);
|
|
2083
|
+
return playEffect(el, pr.effect, pr.options);
|
|
2084
|
+
}
|
|
2085
|
+
const THEME_FX = [
|
|
2086
|
+
{
|
|
2087
|
+
name: 'neon-flicker',
|
|
2088
|
+
kind: 'attention',
|
|
2089
|
+
description: 'A neon tube flickering on — dims (never blacks out) twice, well under 3 flashes per second.',
|
|
2090
|
+
defaults: { color: '#22d3ee' },
|
|
2091
|
+
run: (el, o, ctx) => ctx.animate(el, [{ opacity: 1, filter: 'none' }, { opacity: 0.55, offset: 0.2 }, { opacity: 1, filter: `drop-shadow(0 0 6px ${o.color})`, offset: 0.35 }, { opacity: 0.7, offset: 0.6 }, { opacity: 1, filter: `drop-shadow(0 0 10px ${o.color})` }], { duration: 900, easing: 'linear' }),
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: 'paper-fold',
|
|
2095
|
+
kind: 'enter',
|
|
2096
|
+
description: 'Unfolds like a sheet of paper hinged at the top.',
|
|
2097
|
+
defaults: { duration: 600 },
|
|
2098
|
+
run: (el, o, ctx) => ctx.animate(el, [{ transform: 'perspective(800px) rotateX(-85deg)', transformOrigin: 'top', opacity: 0 }, { transform: 'perspective(800px) rotateX(12deg)', transformOrigin: 'top', opacity: 1, offset: 0.7 }, { transform: 'perspective(800px) rotateX(0)', transformOrigin: 'top', opacity: 1 }], { duration: o.duration, easing: 'ease-out', fill: 'backwards' }),
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
name: 'glass-shine',
|
|
2102
|
+
kind: 'hover',
|
|
2103
|
+
description: 'A bright diagonal shine sweeps across frosted glass.',
|
|
2104
|
+
defaults: { duration: 700 },
|
|
2105
|
+
run: (el, o, ctx) => {
|
|
2106
|
+
const [s, remove] = overlay(el, 'overflow:hidden;background:linear-gradient(105deg,transparent 35%,#ffffff8c 50%,transparent 65%);background-size:250% 100%;background-position:120% 0');
|
|
2107
|
+
const a = ctx.animate(s, [{ backgroundPosition: '120% 0' }, { backgroundPosition: '-20% 0' }], { duration: o.duration, easing: 'ease-in-out' });
|
|
2108
|
+
return a ? a.finished.then(remove, remove) : void remove();
|
|
2109
|
+
},
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
name: 'retro-scanlines',
|
|
2113
|
+
kind: 'background',
|
|
2114
|
+
description: 'CRT scanlines with a slow roll (static lines under reduced motion; persistent).',
|
|
2115
|
+
reduced: 'run',
|
|
2116
|
+
defaults: { opacity: 0.18 },
|
|
2117
|
+
run: (el, o, ctx) => {
|
|
2118
|
+
const [s, remove] = overlay(el, `opacity:${o.opacity};background:repeating-linear-gradient(0deg,#000 0 1px,transparent 1px 3px);mix-blend-mode:multiply`);
|
|
2119
|
+
const a = ctx.reduced ? null : ctx.animate(s, [{ backgroundPosition: '0 0' }, { backgroundPosition: '0 30px' }], { duration: 2400, iterations: Infinity, easing: 'linear' });
|
|
2120
|
+
return () => {
|
|
2121
|
+
a?.cancel();
|
|
2122
|
+
remove();
|
|
2123
|
+
};
|
|
2124
|
+
},
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
name: 'brutal-shift',
|
|
2128
|
+
kind: 'click',
|
|
2129
|
+
description: 'Presses into its hard drop shadow and springs back (brutalist buttons).',
|
|
2130
|
+
defaults: { offset: 6 },
|
|
2131
|
+
run: (el, o, ctx) => ctx.animate(el, [{ transform: 'translate(0,0)' }, { transform: `translate(${o.offset}px,${o.offset}px)`, boxShadow: '0 0 0 #000', offset: 0.3 }, { transform: 'translate(0,0)' }], { duration: 260, easing: 'ease-out' }),
|
|
2132
|
+
},
|
|
2133
|
+
];
|
|
2134
|
+
/** `<usa-theme name="neon | paper | glass | retro | brutalist">` — a themed subtree. */
|
|
2135
|
+
function defineTheme(tag = 'usa-theme') {
|
|
2136
|
+
return defineElement(tag, (Base) => class UsaTheme extends Base {
|
|
2137
|
+
static get observedAttributes() {
|
|
2138
|
+
return ['name'];
|
|
2139
|
+
}
|
|
2140
|
+
get theme() {
|
|
2141
|
+
const n = this.str('name', 'neon');
|
|
2142
|
+
return THEMES[n] ? n : 'neon';
|
|
2143
|
+
}
|
|
2144
|
+
mount() {
|
|
2145
|
+
this.onCleanup(applyTheme(this.theme, this));
|
|
2146
|
+
this.querySelectorAll('[data-theme-fx]').forEach((el) => {
|
|
2147
|
+
const role = (el.dataset.themeFx || 'click');
|
|
2148
|
+
if (!THEME_ROLES.includes(role))
|
|
2149
|
+
return;
|
|
2150
|
+
const pr = themePreset(this.theme, role);
|
|
2151
|
+
const trigger = role === 'attention' ? 'click' : role === 'background' ? 'load' : role;
|
|
2152
|
+
try {
|
|
2153
|
+
this.onCleanup(bindEffect(el, pr.effect, { ...(pr.options || {}), trigger }));
|
|
2154
|
+
}
|
|
2155
|
+
catch {
|
|
2156
|
+
/* effect not registered: call registerAllEffects() */
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}, { id: 'usa-theme-el', text: 'usa-theme{display:block}' });
|
|
2161
|
+
}
|
|
2162
|
+
|
|
1442
2163
|
/**
|
|
1443
2164
|
* use-scroll-animate/components/effects — the 5.x effect packs, all
|
|
1444
2165
|
* registered through `registerEffect()` (5.0) and playable with
|
|
@@ -1458,6 +2179,9 @@ const EFFECT_PACKS = {
|
|
|
1458
2179
|
page: PAGE_FX,
|
|
1459
2180
|
generative: GENERATIVE_FX,
|
|
1460
2181
|
audio: AUDIO_FX,
|
|
2182
|
+
cursor: CURSOR_FX,
|
|
2183
|
+
micro: MICRO_FX,
|
|
2184
|
+
themes: THEME_FX,
|
|
1461
2185
|
};
|
|
1462
2186
|
/** 5.1: card & click effects 2.0. */
|
|
1463
2187
|
function registerCardClickEffects() {
|
|
@@ -1479,10 +2203,21 @@ function registerGenerativeEffects() {
|
|
|
1479
2203
|
function registerAudioEffects() {
|
|
1480
2204
|
registerEffects(EFFECT_PACKS.audio);
|
|
1481
2205
|
}
|
|
2206
|
+
/** 5.7: cursor trails, magnetic dots, spotlight cursor. */
|
|
2207
|
+
function registerCursorEffects() {
|
|
2208
|
+
registerEffects(EFFECT_PACKS.cursor);
|
|
2209
|
+
}
|
|
2210
|
+
/** 5.8: micro-interactions + theme-pack effects. */
|
|
2211
|
+
function registerMicroEffects() {
|
|
2212
|
+
registerEffects(EFFECT_PACKS.micro);
|
|
2213
|
+
registerEffects(EFFECT_PACKS.themes);
|
|
2214
|
+
}
|
|
1482
2215
|
/** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
|
|
1483
2216
|
function defineEffectElements() {
|
|
1484
2217
|
defineStory();
|
|
1485
2218
|
defineAudio();
|
|
2219
|
+
defineGestureFx();
|
|
2220
|
+
defineTheme();
|
|
1486
2221
|
}
|
|
1487
2222
|
/** Register the built-ins and every pack (idempotent). */
|
|
1488
2223
|
function registerAllEffects() {
|
|
@@ -1491,5 +2226,5 @@ function registerAllEffects() {
|
|
|
1491
2226
|
registerEffects(defs);
|
|
1492
2227
|
}
|
|
1493
2228
|
|
|
1494
|
-
export { AUDIO_FX, CARD_FX, CLICK_FX, EFFECT_PACKS, GENERATIVE_FX, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, bindBeat, bounceKeyframes, canvasBackground, createBeatDetector, defineAudio, defineEffectElements, defineStory, disableAudio, enableAudio, formatCount, fxLayer, getAudio, hexRgb, noise2, onBeat, registerAllEffects, registerAudioEffects, registerCardClickEffects, registerGenerativeEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
|
|
2229
|
+
export { AUDIO_FX, CARD_FX, CLICK_FX, CURSOR_FX, EFFECT_PACKS, GENERATIVE_FX, GESTURES, MICRO_FX, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, THEMES, THEME_FX, THEME_NAMES, THEME_ROLES, angleDelta, applyTheme, bindBeat, bindGesture, bounceKeyframes, bumpCount, canvasBackground, createBeatDetector, defineAudio, defineEffectElements, defineGestureFx, defineStory, defineTheme, disableAudio, enableAudio, flingVelocity, formatCount, fxLayer, getAudio, hexRgb, noise2, onBeat, playThemeEffect, registerAllEffects, registerAudioEffects, registerCardClickEffects, registerCursorEffects, registerGenerativeEffects, registerMicroEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress, swapLabel, themeCss, themePreset, themeVars, togglePressed };
|
|
1495
2230
|
//# sourceMappingURL=effects.js.map
|