use-scroll-animate 5.6.0 → 5.7.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 CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.7.0] - 2026-10-08
11
+
12
+ ### Added
13
+ - **Cursor pack** (`use-scroll-animate/components/effects`, `registerCursorEffects()`, `CURSOR_FX`, kind `cursor`, persistent, scoped to the bound element): `comet-trail` (`color`, `width`, `life`), `ribbon-trail` (rainbow, `width`, `life`), `sparkle-trail` (`colors`, `spacing`, `size`), `magnetic-dots` (dot grid behind the content leaning toward the pointer; `gap`, `radius`, `color`), `spotlight-cursor` (eased soft light; `color`, `size`, `ease`). Trails draw on one fixed, pointer-transparent overlay canvas that only animates while a tail is fading. Mouse / pen only by default — `touch: true` opts touch in.
14
+ - **Gestures → effects**: `bindGesture(el, 'fling' | 'twist' | 'long-press', effectNameOrCallback, { velocity, angle, duration, tolerance, effectOptions })` — fling = fast release (px/ms, with direction), twist = two-finger rotation past `angle`° (`cw` / `ccw`), long press charges `--usa-charge` 0 → 1 (`data-charging` while charging) and fires when full; moving cancels. Every fire dispatches `usa-gesture`. Pure helpers `flingVelocity()`, `angleDelta()`.
15
+ - `<usa-gesture-fx gesture effect options velocity angle duration [self]>` — plays the effect on its first child.
16
+ - Showcase: **Cursor trails** and **Gesture triggers** cards.
17
+
18
+ ### Accessibility
19
+ - Cursor effects are skipped under reduced motion; gesture-fired effects go through `playEffect()`, which applies reduced motion. Gestures only add effects — they never replace a click / keyboard action.
20
+
10
21
  ## [5.6.0] - 2026-10-08
11
22
 
12
23
  ### Added
@@ -1441,6 +1441,388 @@ function defineAudio(tag = 'usa-audio') {
1441
1441
  }, { id: 'usa-audio', text: 'usa-audio{display:block}usa-audio[data-audio-error] [data-audio-toggle]{outline:2px solid #ff5c8a}' });
1442
1442
  }
1443
1443
 
1444
+ const touchOk = (e, o) => o.touch || e.pointerType !== 'touch';
1445
+ /** A fixed, viewport-sized canvas in the fx layer, redrawn while `draw` returns true. */
1446
+ function overlayCanvas(draw) {
1447
+ const c = document.createElement('canvas');
1448
+ c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%';
1449
+ fxLayer().appendChild(c);
1450
+ const g = c.getContext('2d');
1451
+ let raf = 0;
1452
+ const frame = (now) => {
1453
+ raf = 0;
1454
+ if (!g)
1455
+ return;
1456
+ const w = innerWidth;
1457
+ const h = innerHeight;
1458
+ const dpr = Math.min(2, devicePixelRatio || 1);
1459
+ if (c.width !== Math.round(w * dpr) || c.height !== Math.round(h * dpr)) {
1460
+ c.width = Math.round(w * dpr);
1461
+ c.height = Math.round(h * dpr);
1462
+ }
1463
+ g.setTransform(dpr, 0, 0, dpr, 0, 0);
1464
+ g.clearRect(0, 0, w, h);
1465
+ if (draw(g, w, h, now))
1466
+ raf = requestAnimationFrame(frame);
1467
+ };
1468
+ return {
1469
+ kick: () => {
1470
+ if (!raf && g)
1471
+ raf = requestAnimationFrame(frame);
1472
+ },
1473
+ stop: () => {
1474
+ cancelAnimationFrame(raf);
1475
+ raf = 0;
1476
+ c.remove();
1477
+ },
1478
+ };
1479
+ }
1480
+ /** Pointer trail on `el`: keeps the last `life` ms of points and redraws them. */
1481
+ function trail(el, o, paint) {
1482
+ const pts = [];
1483
+ const oc = overlayCanvas((g, _w, _h, now) => {
1484
+ while (pts.length && now - pts[0].t > o.life)
1485
+ pts.shift();
1486
+ if (pts.length > 1)
1487
+ paint(g, pts, now);
1488
+ return pts.length > 0;
1489
+ });
1490
+ const move = (e) => {
1491
+ if (!touchOk(e, o))
1492
+ return;
1493
+ pts.push({ x: e.clientX, y: e.clientY, t: performance.now() });
1494
+ if (pts.length > 64)
1495
+ pts.shift();
1496
+ oc.kick();
1497
+ };
1498
+ el.addEventListener('pointermove', move, { passive: true });
1499
+ return () => {
1500
+ el.removeEventListener('pointermove', move);
1501
+ oc.stop();
1502
+ };
1503
+ }
1504
+ const CURSOR_FX = [
1505
+ {
1506
+ name: 'comet-trail',
1507
+ kind: 'cursor',
1508
+ description: 'A glowing comet tail follows the pointer over the element (persistent; `color`, `width`, `life`).',
1509
+ defaults: { color: '#7c5cff', width: 10, life: 320, touch: false },
1510
+ run: (el, o) => trail(el, o, (g, pts, now) => {
1511
+ const [r, gg, b] = hexRgb(o.color);
1512
+ g.lineCap = 'round';
1513
+ for (let i = 1; i < pts.length; i++) {
1514
+ const k = 1 - (now - pts[i].t) / o.life;
1515
+ g.strokeStyle = `rgba(${r},${gg},${b},${(k * 0.9).toFixed(3)})`;
1516
+ g.lineWidth = Math.max(0.5, o.width * k);
1517
+ g.beginPath();
1518
+ g.moveTo(pts[i - 1].x, pts[i - 1].y);
1519
+ g.lineTo(pts[i].x, pts[i].y);
1520
+ g.stroke();
1521
+ }
1522
+ }),
1523
+ },
1524
+ {
1525
+ name: 'ribbon-trail',
1526
+ kind: 'cursor',
1527
+ description: 'A smooth rainbow ribbon flows behind the pointer (persistent; `width`, `life`).',
1528
+ defaults: { width: 18, life: 600, touch: false },
1529
+ run: (el, o) => trail(el, o, (g, pts, now) => {
1530
+ for (let i = 2; i < pts.length; i++) {
1531
+ const k = 1 - (now - pts[i].t) / o.life;
1532
+ const a = pts[i - 2];
1533
+ const m = pts[i - 1];
1534
+ const b = pts[i];
1535
+ g.strokeStyle = `hsla(${(pts[i].t / 6) % 360},90%,62%,${(k * 0.85).toFixed(3)})`;
1536
+ g.lineWidth = Math.max(0.5, o.width * k * Math.sin(Math.PI * Math.min(1, (i / pts.length) * 1.1)));
1537
+ g.lineCap = 'round';
1538
+ g.beginPath();
1539
+ g.moveTo((a.x + m.x) / 2, (a.y + m.y) / 2);
1540
+ g.quadraticCurveTo(m.x, m.y, (m.x + b.x) / 2, (m.y + b.y) / 2);
1541
+ g.stroke();
1542
+ }
1543
+ }),
1544
+ },
1545
+ {
1546
+ name: 'sparkle-trail',
1547
+ kind: 'cursor',
1548
+ description: 'Little stars twinkle off the pointer as it moves (persistent; `colors`, `spacing` px between stars).',
1549
+ defaults: { colors: PALETTE, spacing: 14, size: 14, touch: false },
1550
+ run: (el, o, ctx) => {
1551
+ let lx = -1e4;
1552
+ let ly = -1e4;
1553
+ const move = (e) => {
1554
+ if (!touchOk(e, o) || Math.hypot(e.clientX - lx, e.clientY - ly) < o.spacing)
1555
+ return;
1556
+ lx = e.clientX;
1557
+ ly = e.clientY;
1558
+ const s = o.size * rand(0.6, 1.2);
1559
+ 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' }, '✦');
1560
+ };
1561
+ el.addEventListener('pointermove', move, { passive: true });
1562
+ return () => el.removeEventListener('pointermove', move);
1563
+ },
1564
+ },
1565
+ {
1566
+ name: 'magnetic-dots',
1567
+ kind: 'cursor',
1568
+ description: 'A grid of dots behind the content leans toward the pointer like iron filings to a magnet (persistent; `gap`, `radius`, `color`).',
1569
+ defaults: { gap: 22, radius: 120, color: '#7c5cff', background: 'transparent', touch: false },
1570
+ reduced: 'skip',
1571
+ run: (el, o, ctx) => {
1572
+ const p = { x: -1e4, y: -1e4 };
1573
+ const move = (e) => {
1574
+ if (!touchOk(e, o))
1575
+ return;
1576
+ const r = el.getBoundingClientRect();
1577
+ p.x = e.clientX - r.left;
1578
+ p.y = e.clientY - r.top;
1579
+ };
1580
+ const leave = () => ((p.x = -1e4), (p.y = -1e4));
1581
+ el.addEventListener('pointermove', move, { passive: true });
1582
+ el.addEventListener('pointerleave', leave);
1583
+ const off = canvasBackground(el, ctx, {
1584
+ draw: ({ ctx: g, w, h }) => {
1585
+ g.clearRect(0, 0, w, h);
1586
+ if (o.background !== 'transparent') {
1587
+ g.fillStyle = o.background;
1588
+ g.fillRect(0, 0, w, h);
1589
+ }
1590
+ g.fillStyle = o.color;
1591
+ for (let y = o.gap / 2; y < h; y += o.gap)
1592
+ for (let x = o.gap / 2; x < w; x += o.gap) {
1593
+ const dx = p.x - x;
1594
+ const dy = p.y - y;
1595
+ const d = Math.hypot(dx, dy);
1596
+ const k = d < o.radius ? (1 - d / o.radius) ** 2 : 0;
1597
+ const s = 1.5 + k * 3;
1598
+ g.globalAlpha = 0.35 + k * 0.65;
1599
+ g.fillRect(x + dx * k * 0.35 - s / 2, y + dy * k * 0.35 - s / 2, s, s);
1600
+ }
1601
+ g.globalAlpha = 1;
1602
+ },
1603
+ }, o);
1604
+ return () => {
1605
+ el.removeEventListener('pointermove', move);
1606
+ el.removeEventListener('pointerleave', leave);
1607
+ off();
1608
+ };
1609
+ },
1610
+ },
1611
+ {
1612
+ name: 'spotlight-cursor',
1613
+ kind: 'cursor',
1614
+ description: 'A soft light follows the pointer across the element, easing behind it (persistent; `color`, `size`).',
1615
+ defaults: { color: '#ffffff', size: 260, ease: 0.18, touch: false },
1616
+ run: (el, o) => {
1617
+ const [r, g, b] = hexRgb(o.color);
1618
+ const s = document.createElement('span');
1619
+ s.setAttribute('aria-hidden', 'true');
1620
+ 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)`;
1621
+ const restore = el.style.position;
1622
+ if (getComputedStyle(el).position === 'static')
1623
+ el.style.position = 'relative';
1624
+ el.appendChild(s);
1625
+ let tx = 0;
1626
+ let ty = 0;
1627
+ let x = 0;
1628
+ let y = 0;
1629
+ let raf = 0;
1630
+ const tick = () => {
1631
+ x += (tx - x) * o.ease;
1632
+ y += (ty - y) * o.ease;
1633
+ s.style.setProperty('--sx', `${x.toFixed(1)}px`);
1634
+ s.style.setProperty('--sy', `${y.toFixed(1)}px`);
1635
+ raf = Math.abs(tx - x) + Math.abs(ty - y) > 0.3 ? requestAnimationFrame(tick) : 0;
1636
+ };
1637
+ const move = (e) => {
1638
+ if (!touchOk(e, o))
1639
+ return;
1640
+ const rc = el.getBoundingClientRect();
1641
+ tx = e.clientX - rc.left;
1642
+ ty = e.clientY - rc.top;
1643
+ if (s.style.opacity !== '1')
1644
+ ((x = tx), (y = ty), (s.style.opacity = '1'));
1645
+ if (!raf)
1646
+ raf = requestAnimationFrame(tick);
1647
+ };
1648
+ const leave = () => (s.style.opacity = '0');
1649
+ el.addEventListener('pointermove', move, { passive: true });
1650
+ el.addEventListener('pointerleave', leave);
1651
+ return () => {
1652
+ cancelAnimationFrame(raf);
1653
+ el.removeEventListener('pointermove', move);
1654
+ el.removeEventListener('pointerleave', leave);
1655
+ s.remove();
1656
+ el.style.position = restore;
1657
+ };
1658
+ },
1659
+ },
1660
+ ];
1661
+ // --- gestures → effects ------------------------------------------------------
1662
+ const GESTURES = ['fling', 'twist', 'long-press'];
1663
+ /** Release velocity (px/ms) from recent pointer samples: uses the last `window` ms (pure). */
1664
+ function flingVelocity(pts, window = 90) {
1665
+ if (pts.length < 2)
1666
+ return { vx: 0, vy: 0, speed: 0 };
1667
+ const last = pts[pts.length - 1];
1668
+ let first = pts[0];
1669
+ for (let i = pts.length - 2; i >= 0; i--) {
1670
+ first = pts[i];
1671
+ if (last.t - pts[i].t >= window)
1672
+ break;
1673
+ }
1674
+ const dt = Math.max(1, last.t - first.t);
1675
+ const vx = (last.x - first.x) / dt;
1676
+ const vy = (last.y - first.y) / dt;
1677
+ return { vx, vy, speed: Math.hypot(vx, vy) };
1678
+ }
1679
+ /** Signed smallest difference between two angles in degrees, in (-180, 180] (pure). */
1680
+ function angleDelta(a, b) {
1681
+ let d = (b - a) % 360;
1682
+ if (d > 180)
1683
+ d -= 360;
1684
+ if (d <= -180)
1685
+ d += 360;
1686
+ return d;
1687
+ }
1688
+ /**
1689
+ * Fire `effect` (a registered effect name, or a callback) when `gesture`
1690
+ * happens on `el`. Returns an unbind.
1691
+ */
1692
+ function bindGesture(el, gesture, effect, o = {}) {
1693
+ const { velocity = 0.8, angle = 30, duration = 650, tolerance = 10, effectOptions = {} } = o;
1694
+ const pts = new Map();
1695
+ let path = [];
1696
+ let base = null;
1697
+ let press = null;
1698
+ const fire = (d, e) => {
1699
+ el.dispatchEvent(new CustomEvent('usa-gesture', { detail: d, bubbles: true }));
1700
+ if (typeof effect === 'function')
1701
+ effect(d, e);
1702
+ else
1703
+ components_fx.playEffect(el, effect, effectOptions, e).catch(() => undefined);
1704
+ };
1705
+ const twistAngle = () => {
1706
+ const [a, b] = [...pts.values()];
1707
+ return (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
1708
+ };
1709
+ const charge = (k) => {
1710
+ el.style.setProperty('--usa-charge', k.toFixed(3));
1711
+ el.toggleAttribute('data-charging', k > 0 && k < 1);
1712
+ };
1713
+ const endPress = () => {
1714
+ if (!press)
1715
+ return;
1716
+ cancelAnimationFrame(press.raf);
1717
+ press = null;
1718
+ charge(0);
1719
+ };
1720
+ const down = (e) => {
1721
+ pts.set(e.pointerId ?? 1, { x: e.clientX, y: e.clientY });
1722
+ if (gesture === 'fling')
1723
+ path = [{ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() }];
1724
+ if (gesture === 'twist' && pts.size === 2)
1725
+ base = twistAngle();
1726
+ if (gesture === 'long-press' && pts.size === 1) {
1727
+ press = { x: e.clientX, y: e.clientY, t0: performance.now(), raf: 0 };
1728
+ const step = (now) => {
1729
+ if (!press)
1730
+ return;
1731
+ const k = Math.min(1, (now - press.t0) / duration);
1732
+ charge(k);
1733
+ if (k >= 1) {
1734
+ endPress();
1735
+ el.style.setProperty('--usa-charge', '1');
1736
+ fire({ gesture, charge: 1 }, e);
1737
+ }
1738
+ else
1739
+ press.raf = requestAnimationFrame(step);
1740
+ };
1741
+ press.raf = requestAnimationFrame(step);
1742
+ }
1743
+ };
1744
+ const move = (e) => {
1745
+ if (!pts.has(e.pointerId ?? 1))
1746
+ return;
1747
+ pts.set(e.pointerId ?? 1, { x: e.clientX, y: e.clientY });
1748
+ if (gesture === 'fling') {
1749
+ path.push({ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() });
1750
+ if (path.length > 20)
1751
+ path.shift();
1752
+ }
1753
+ else if (gesture === 'twist' && pts.size >= 2 && base !== null) {
1754
+ const now = twistAngle();
1755
+ const d = angleDelta(base, now);
1756
+ if (Math.abs(d) >= angle) {
1757
+ base = now;
1758
+ fire({ gesture, angle: d, direction: d > 0 ? 'cw' : 'ccw' }, e);
1759
+ }
1760
+ }
1761
+ else if (gesture === 'long-press' && press && Math.hypot(e.clientX - press.x, e.clientY - press.y) > tolerance)
1762
+ endPress();
1763
+ };
1764
+ const up = (e) => {
1765
+ pts.delete(e.pointerId ?? 1);
1766
+ if (gesture === 'fling' && path.length) {
1767
+ path.push({ x: e.clientX, y: e.clientY, t: e.timeStamp || performance.now() });
1768
+ const v = flingVelocity(path);
1769
+ path = [];
1770
+ if (v.speed >= velocity) {
1771
+ const direction = Math.abs(v.vx) > Math.abs(v.vy) ? (v.vx > 0 ? 'right' : 'left') : v.vy > 0 ? 'down' : 'up';
1772
+ fire({ gesture, ...v, direction }, e);
1773
+ }
1774
+ }
1775
+ if (pts.size < 2)
1776
+ base = null;
1777
+ endPress();
1778
+ };
1779
+ el.addEventListener('pointerdown', down);
1780
+ el.addEventListener('pointermove', move);
1781
+ el.addEventListener('pointerup', up);
1782
+ el.addEventListener('pointercancel', up);
1783
+ if (gesture === 'twist' && !el.style.touchAction)
1784
+ el.style.touchAction = 'none';
1785
+ return () => {
1786
+ endPress();
1787
+ el.removeEventListener('pointerdown', down);
1788
+ el.removeEventListener('pointermove', move);
1789
+ el.removeEventListener('pointerup', up);
1790
+ el.removeEventListener('pointercancel', up);
1791
+ };
1792
+ }
1793
+ /**
1794
+ * `<usa-gesture-fx gesture="fling | twist | long-press" effect="tada"
1795
+ * options='{"…"}' velocity angle duration>` — plays `effect` on its first
1796
+ * child (or itself with `self`) when the gesture happens.
1797
+ */
1798
+ function defineGestureFx(tag = 'usa-gesture-fx') {
1799
+ return base.defineElement(tag, (Base) => class UsaGestureFx extends Base {
1800
+ static get observedAttributes() {
1801
+ return ['gesture', 'effect', 'options'];
1802
+ }
1803
+ get gesture() {
1804
+ const g = this.str('gesture', 'fling');
1805
+ return GESTURES.includes(g) ? g : 'fling';
1806
+ }
1807
+ mount() {
1808
+ let effectOptions = {};
1809
+ try {
1810
+ effectOptions = JSON.parse(this.str('options', '{}')) || {};
1811
+ }
1812
+ catch {
1813
+ /* ignore bad JSON */
1814
+ }
1815
+ const target = this.flag('self') ? this : this.firstElementChild || this;
1816
+ const effect = this.str('effect', 'pulse');
1817
+ this.onCleanup(bindGesture(this, this.gesture, (_d, e) => void components_fx.playEffect(target, effect, effectOptions, e).catch(() => undefined), {
1818
+ velocity: this.num('velocity', 0.8),
1819
+ angle: this.num('angle', 30),
1820
+ duration: this.num('duration', 650),
1821
+ }));
1822
+ }
1823
+ }, { id: 'usa-gesture-fx', text: 'usa-gesture-fx{display:inline-block;touch-action:none;user-select:none}' });
1824
+ }
1825
+
1444
1826
  /**
1445
1827
  * use-scroll-animate/components/effects — the 5.x effect packs, all
1446
1828
  * registered through `registerEffect()` (5.0) and playable with
@@ -1460,6 +1842,7 @@ const EFFECT_PACKS = {
1460
1842
  page: PAGE_FX,
1461
1843
  generative: GENERATIVE_FX,
1462
1844
  audio: AUDIO_FX,
1845
+ cursor: CURSOR_FX,
1463
1846
  };
1464
1847
  /** 5.1: card & click effects 2.0. */
1465
1848
  function registerCardClickEffects() {
@@ -1481,10 +1864,15 @@ function registerGenerativeEffects() {
1481
1864
  function registerAudioEffects() {
1482
1865
  components_fx.registerEffects(EFFECT_PACKS.audio);
1483
1866
  }
1867
+ /** 5.7: cursor trails, magnetic dots, spotlight cursor. */
1868
+ function registerCursorEffects() {
1869
+ components_fx.registerEffects(EFFECT_PACKS.cursor);
1870
+ }
1484
1871
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
1485
1872
  function defineEffectElements() {
1486
1873
  defineStory();
1487
1874
  defineAudio();
1875
+ defineGestureFx();
1488
1876
  }
1489
1877
  /** Register the built-ins and every pack (idempotent). */
1490
1878
  function registerAllEffects() {
@@ -1496,20 +1884,26 @@ function registerAllEffects() {
1496
1884
  exports.AUDIO_FX = AUDIO_FX;
1497
1885
  exports.CARD_FX = CARD_FX;
1498
1886
  exports.CLICK_FX = CLICK_FX;
1887
+ exports.CURSOR_FX = CURSOR_FX;
1499
1888
  exports.EFFECT_PACKS = EFFECT_PACKS;
1500
1889
  exports.GENERATIVE_FX = GENERATIVE_FX;
1890
+ exports.GESTURES = GESTURES;
1501
1891
  exports.PAGE_FX = PAGE_FX;
1502
1892
  exports.PHYSICS_FX = PHYSICS_FX;
1503
1893
  exports.STORY_TEMPLATES = STORY_TEMPLATES;
1894
+ exports.angleDelta = angleDelta;
1504
1895
  exports.bindBeat = bindBeat;
1896
+ exports.bindGesture = bindGesture;
1505
1897
  exports.bounceKeyframes = bounceKeyframes;
1506
1898
  exports.canvasBackground = canvasBackground;
1507
1899
  exports.createBeatDetector = createBeatDetector;
1508
1900
  exports.defineAudio = defineAudio;
1509
1901
  exports.defineEffectElements = defineEffectElements;
1902
+ exports.defineGestureFx = defineGestureFx;
1510
1903
  exports.defineStory = defineStory;
1511
1904
  exports.disableAudio = disableAudio;
1512
1905
  exports.enableAudio = enableAudio;
1906
+ exports.flingVelocity = flingVelocity;
1513
1907
  exports.formatCount = formatCount;
1514
1908
  exports.fxLayer = fxLayer;
1515
1909
  exports.getAudio = getAudio;
@@ -1519,6 +1913,7 @@ exports.onBeat = onBeat;
1519
1913
  exports.registerAllEffects = registerAllEffects;
1520
1914
  exports.registerAudioEffects = registerAudioEffects;
1521
1915
  exports.registerCardClickEffects = registerCardClickEffects;
1916
+ exports.registerCursorEffects = registerCursorEffects;
1522
1917
  exports.registerGenerativeEffects = registerGenerativeEffects;
1523
1918
  exports.registerPageEffects = registerPageEffects;
1524
1919
  exports.registerPhysicsEffects = registerPhysicsEffects;