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 +11 -0
- package/dist/components/effects.cjs +395 -0
- package/dist/components/effects.cjs.map +1 -1
- package/dist/components/effects.d.cts +75 -2
- package/dist/components/effects.d.ts +75 -2
- package/dist/components/effects.js +389 -1
- 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 +1 -1
- package/docs/components.md +16 -0
- package/package.json +1 -1
|
@@ -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
|