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.
@@ -235,6 +235,77 @@ interface UsaAudioElement extends UsaElement {
235
235
  */
236
236
  declare function defineAudio(tag?: string): CustomElementConstructor | undefined;
237
237
 
238
+ /**
239
+ * 5.7 — cursor & gesture packs.
240
+ *
241
+ * Cursor effects (kind `cursor`, persistent, scoped to the element they are
242
+ * bound to; skipped under reduced motion and — unless `touch: true` — for
243
+ * touch pointers): `comet-trail`, `sparkle-trail`, `ribbon-trail`,
244
+ * `magnetic-dots`, `spotlight-cursor`.
245
+ *
246
+ * Gestures → effects: `bindGesture(el, 'fling' | 'twist' | 'long-press',
247
+ * effectOrCallback, options)` and `<usa-gesture-fx gesture effect>`. A fling
248
+ * is a fast release, a twist a two-finger rotation past `angle` degrees, a
249
+ * long press "charges" `--usa-charge` 0 → 1 and fires when full. Every fire
250
+ * dispatches `usa-gesture` (`detail: { gesture, … }`). The effect itself goes
251
+ * through `playEffect()`, so reduced motion is honoured there.
252
+ */
253
+
254
+ type Pt = {
255
+ x: number;
256
+ y: number;
257
+ t: number;
258
+ };
259
+ declare const CURSOR_FX: EffectDefinition[];
260
+ declare const GESTURES: readonly ["fling", "twist", "long-press"];
261
+ type GestureName = (typeof GESTURES)[number];
262
+ interface GestureFxOptions {
263
+ /** fling: minimum release speed in px/ms (default 0.8). */
264
+ velocity?: number;
265
+ /** twist: degrees of rotation that fire (default 30). */
266
+ angle?: number;
267
+ /** long-press: ms to fully charge (default 650). */
268
+ duration?: number;
269
+ /** long-press: px the pointer may move before the press is cancelled (default 10). */
270
+ tolerance?: number;
271
+ /** Options passed to the effect. */
272
+ effectOptions?: Record<string, unknown>;
273
+ }
274
+ interface GestureDetail {
275
+ gesture: GestureName;
276
+ /** fling */
277
+ vx?: number;
278
+ vy?: number;
279
+ speed?: number;
280
+ direction?: 'left' | 'right' | 'up' | 'down' | 'cw' | 'ccw';
281
+ /** twist: signed degrees since the last fire. */
282
+ angle?: number;
283
+ /** long-press: 1 when fired. */
284
+ charge?: number;
285
+ }
286
+ /** Release velocity (px/ms) from recent pointer samples: uses the last `window` ms (pure). */
287
+ declare function flingVelocity(pts: Pt[], window?: number): {
288
+ vx: number;
289
+ vy: number;
290
+ speed: number;
291
+ };
292
+ /** Signed smallest difference between two angles in degrees, in (-180, 180] (pure). */
293
+ declare function angleDelta(a: number, b: number): number;
294
+ /**
295
+ * Fire `effect` (a registered effect name, or a callback) when `gesture`
296
+ * happens on `el`. Returns an unbind.
297
+ */
298
+ declare function bindGesture(el: HTMLElement, gesture: GestureName, effect: string | ((d: GestureDetail, e: Event) => void), o?: GestureFxOptions): () => void;
299
+ interface UsaGestureFxElement extends UsaElement {
300
+ readonly gesture: GestureName;
301
+ }
302
+ /**
303
+ * `<usa-gesture-fx gesture="fling | twist | long-press" effect="tada"
304
+ * options='{"…"}' velocity angle duration>` — plays `effect` on its first
305
+ * child (or itself with `self`) when the gesture happens.
306
+ */
307
+ declare function defineGestureFx(tag?: string): CustomElementConstructor | undefined;
308
+
238
309
  /**
239
310
  * 5.4 — `<usa-story template="…">` scroll-storytelling templates.
240
311
  *
@@ -289,10 +360,12 @@ declare function registerPageEffects(): void;
289
360
  declare function registerGenerativeEffects(): void;
290
361
  /** 5.6: sound-reactive (Web Audio) backgrounds. */
291
362
  declare function registerAudioEffects(): void;
363
+ /** 5.7: cursor trails, magnetic dots, spotlight cursor. */
364
+ declare function registerCursorEffects(): void;
292
365
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
293
366
  declare function defineEffectElements(): void;
294
367
  /** Register the built-ins and every pack (idempotent). */
295
368
  declare function registerAllEffects(): void;
296
369
 
297
- 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 };
298
- export type { AudioInput, AudioReactive, AudioSample, BeatOptions, GenFrame, GenerativeSpec, SpringOptions, StoryTemplate, UsaAudioElement, UsaStoryElement };
370
+ export { AUDIO_FX, CARD_FX, CLICK_FX, CURSOR_FX, EFFECT_PACKS, GENERATIVE_FX, GESTURES, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, angleDelta, bindBeat, bindGesture, bounceKeyframes, canvasBackground, createBeatDetector, defineAudio, defineEffectElements, defineGestureFx, defineStory, disableAudio, enableAudio, flingVelocity, formatCount, fxLayer, getAudio, hexRgb, noise2, onBeat, registerAllEffects, registerAudioEffects, registerCardClickEffects, registerCursorEffects, registerGenerativeEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
371
+ export type { AudioInput, AudioReactive, AudioSample, BeatOptions, GenFrame, GenerativeSpec, GestureDetail, GestureFxOptions, GestureName, SpringOptions, StoryTemplate, UsaAudioElement, UsaGestureFxElement, UsaStoryElement };
@@ -235,6 +235,77 @@ interface UsaAudioElement extends UsaElement {
235
235
  */
236
236
  declare function defineAudio(tag?: string): CustomElementConstructor | undefined;
237
237
 
238
+ /**
239
+ * 5.7 — cursor & gesture packs.
240
+ *
241
+ * Cursor effects (kind `cursor`, persistent, scoped to the element they are
242
+ * bound to; skipped under reduced motion and — unless `touch: true` — for
243
+ * touch pointers): `comet-trail`, `sparkle-trail`, `ribbon-trail`,
244
+ * `magnetic-dots`, `spotlight-cursor`.
245
+ *
246
+ * Gestures → effects: `bindGesture(el, 'fling' | 'twist' | 'long-press',
247
+ * effectOrCallback, options)` and `<usa-gesture-fx gesture effect>`. A fling
248
+ * is a fast release, a twist a two-finger rotation past `angle` degrees, a
249
+ * long press "charges" `--usa-charge` 0 → 1 and fires when full. Every fire
250
+ * dispatches `usa-gesture` (`detail: { gesture, … }`). The effect itself goes
251
+ * through `playEffect()`, so reduced motion is honoured there.
252
+ */
253
+
254
+ type Pt = {
255
+ x: number;
256
+ y: number;
257
+ t: number;
258
+ };
259
+ declare const CURSOR_FX: EffectDefinition[];
260
+ declare const GESTURES: readonly ["fling", "twist", "long-press"];
261
+ type GestureName = (typeof GESTURES)[number];
262
+ interface GestureFxOptions {
263
+ /** fling: minimum release speed in px/ms (default 0.8). */
264
+ velocity?: number;
265
+ /** twist: degrees of rotation that fire (default 30). */
266
+ angle?: number;
267
+ /** long-press: ms to fully charge (default 650). */
268
+ duration?: number;
269
+ /** long-press: px the pointer may move before the press is cancelled (default 10). */
270
+ tolerance?: number;
271
+ /** Options passed to the effect. */
272
+ effectOptions?: Record<string, unknown>;
273
+ }
274
+ interface GestureDetail {
275
+ gesture: GestureName;
276
+ /** fling */
277
+ vx?: number;
278
+ vy?: number;
279
+ speed?: number;
280
+ direction?: 'left' | 'right' | 'up' | 'down' | 'cw' | 'ccw';
281
+ /** twist: signed degrees since the last fire. */
282
+ angle?: number;
283
+ /** long-press: 1 when fired. */
284
+ charge?: number;
285
+ }
286
+ /** Release velocity (px/ms) from recent pointer samples: uses the last `window` ms (pure). */
287
+ declare function flingVelocity(pts: Pt[], window?: number): {
288
+ vx: number;
289
+ vy: number;
290
+ speed: number;
291
+ };
292
+ /** Signed smallest difference between two angles in degrees, in (-180, 180] (pure). */
293
+ declare function angleDelta(a: number, b: number): number;
294
+ /**
295
+ * Fire `effect` (a registered effect name, or a callback) when `gesture`
296
+ * happens on `el`. Returns an unbind.
297
+ */
298
+ declare function bindGesture(el: HTMLElement, gesture: GestureName, effect: string | ((d: GestureDetail, e: Event) => void), o?: GestureFxOptions): () => void;
299
+ interface UsaGestureFxElement extends UsaElement {
300
+ readonly gesture: GestureName;
301
+ }
302
+ /**
303
+ * `<usa-gesture-fx gesture="fling | twist | long-press" effect="tada"
304
+ * options='{"…"}' velocity angle duration>` — plays `effect` on its first
305
+ * child (or itself with `self`) when the gesture happens.
306
+ */
307
+ declare function defineGestureFx(tag?: string): CustomElementConstructor | undefined;
308
+
238
309
  /**
239
310
  * 5.4 — `<usa-story template="…">` scroll-storytelling templates.
240
311
  *
@@ -289,10 +360,12 @@ declare function registerPageEffects(): void;
289
360
  declare function registerGenerativeEffects(): void;
290
361
  /** 5.6: sound-reactive (Web Audio) backgrounds. */
291
362
  declare function registerAudioEffects(): void;
363
+ /** 5.7: cursor trails, magnetic dots, spotlight cursor. */
364
+ declare function registerCursorEffects(): void;
292
365
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
293
366
  declare function defineEffectElements(): void;
294
367
  /** Register the built-ins and every pack (idempotent). */
295
368
  declare function registerAllEffects(): void;
296
369
 
297
- 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 };
298
- export type { AudioInput, AudioReactive, AudioSample, BeatOptions, GenFrame, GenerativeSpec, SpringOptions, StoryTemplate, UsaAudioElement, UsaStoryElement };
370
+ export { AUDIO_FX, CARD_FX, CLICK_FX, CURSOR_FX, EFFECT_PACKS, GENERATIVE_FX, GESTURES, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, angleDelta, bindBeat, bindGesture, bounceKeyframes, canvasBackground, createBeatDetector, defineAudio, defineEffectElements, defineGestureFx, defineStory, disableAudio, enableAudio, flingVelocity, formatCount, fxLayer, getAudio, hexRgb, noise2, onBeat, registerAllEffects, registerAudioEffects, registerCardClickEffects, registerCursorEffects, registerGenerativeEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
371
+ export type { AudioInput, AudioReactive, AudioSample, BeatOptions, GenFrame, GenerativeSpec, GestureDetail, GestureFxOptions, GestureName, SpringOptions, StoryTemplate, UsaAudioElement, UsaGestureFxElement, UsaStoryElement };
@@ -1439,6 +1439,388 @@ 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
+
1442
1824
  /**
1443
1825
  * use-scroll-animate/components/effects — the 5.x effect packs, all
1444
1826
  * registered through `registerEffect()` (5.0) and playable with
@@ -1458,6 +1840,7 @@ const EFFECT_PACKS = {
1458
1840
  page: PAGE_FX,
1459
1841
  generative: GENERATIVE_FX,
1460
1842
  audio: AUDIO_FX,
1843
+ cursor: CURSOR_FX,
1461
1844
  };
1462
1845
  /** 5.1: card & click effects 2.0. */
1463
1846
  function registerCardClickEffects() {
@@ -1479,10 +1862,15 @@ function registerGenerativeEffects() {
1479
1862
  function registerAudioEffects() {
1480
1863
  registerEffects(EFFECT_PACKS.audio);
1481
1864
  }
1865
+ /** 5.7: cursor trails, magnetic dots, spotlight cursor. */
1866
+ function registerCursorEffects() {
1867
+ registerEffects(EFFECT_PACKS.cursor);
1868
+ }
1482
1869
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
1483
1870
  function defineEffectElements() {
1484
1871
  defineStory();
1485
1872
  defineAudio();
1873
+ defineGestureFx();
1486
1874
  }
1487
1875
  /** Register the built-ins and every pack (idempotent). */
1488
1876
  function registerAllEffects() {
@@ -1491,5 +1879,5 @@ function registerAllEffects() {
1491
1879
  registerEffects(defs);
1492
1880
  }
1493
1881
 
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 };
1882
+ export { AUDIO_FX, CARD_FX, CLICK_FX, CURSOR_FX, EFFECT_PACKS, GENERATIVE_FX, GESTURES, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, angleDelta, bindBeat, bindGesture, bounceKeyframes, canvasBackground, createBeatDetector, defineAudio, defineEffectElements, defineGestureFx, defineStory, disableAudio, enableAudio, flingVelocity, formatCount, fxLayer, getAudio, hexRgb, noise2, onBeat, registerAllEffects, registerAudioEffects, registerCardClickEffects, registerCursorEffects, registerGenerativeEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
1495
1883
  //# sourceMappingURL=effects.js.map