use-scroll-animate 5.2.0 → 5.4.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 +27 -0
- package/dist/components/effects.cjs +407 -1
- package/dist/components/effects.cjs.map +1 -1
- package/dist/components/effects.d.cts +65 -2
- package/dist/components/effects.d.ts +65 -2
- package/dist/components/effects.js +401 -2
- 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 +35 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.4.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`<usa-story template="…">`** (`defineStory()` from `use-scroll-animate/components/effects`; `defineEffectElements()` defines every element of the entry) — six scroll-storytelling templates:
|
|
14
|
+
- `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the active step gets `data-active`, the stage `data-active-step="<i>"`.
|
|
15
|
+
- `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
|
|
16
|
+
- `zoom` — zoom-through: the stage scales up to `zoom="6"` and fades.
|
|
17
|
+
- `compare` — before / after wipe driven by scroll, with a draggable, keyboard-accessible handle (`role="slider"`, ←/→, Shift for ×5, Home/End).
|
|
18
|
+
- `counter` — `[data-count="12,480"]` numbers count up on entering view (separators, decimals, prefix / suffix kept).
|
|
19
|
+
- `highlight` — the paragraph (or `[data-step]`) crossing the viewport center is highlighted.
|
|
20
|
+
- Every template sets `--usa-story-progress` (0–1), exposes `progress` / `step` / `update()` and fires `usa-story-step`. Helpers: `storyProgress(el)`, `formatCount(target, t)`, `STORY_TEMPLATES`.
|
|
21
|
+
- Showcase: new **[Scroll stories](./showcase/story.html)** page with all six templates full-page; gallery cards **Story: before / after**, **Story: data counters**, **Story: step highlight**. Gallery code tabs import from the card’s own entry.
|
|
22
|
+
|
|
23
|
+
### Accessibility
|
|
24
|
+
- Under reduced motion nothing slides or zooms (the gallery stacks vertically), counters show their final values immediately; counted numbers carry the final value as `aria-label`.
|
|
25
|
+
|
|
26
|
+
## [5.3.0] - 2026-10-08
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Page-wide pack** (`use-scroll-animate/components/effects`, `registerPageEffects()`, `PAGE_FX`):
|
|
30
|
+
- Transitions — `curtain`, `iris` (closes on the click point), `pixel-dissolve` (`cols` × `rows`), `blinds` (`slats`). Each covers the viewport, awaits `onCovered()` (swap your route / content there), optionally `hold`s, then reveals; `playEffect()` resolves when the page is visible again.
|
|
31
|
+
- Persistent — `velocity-skew` (skews with scroll speed, eases back), `spotlight` (dims everything but a circle at the pointer), `edge-glow` (lights the viewport edge you scroll toward).
|
|
32
|
+
- Showcase: **Page transitions** and **Velocity skew & edge glow** cards.
|
|
33
|
+
|
|
34
|
+
### Accessibility
|
|
35
|
+
- Transition layers are `aria-hidden`; under reduced motion every transition becomes a 150 ms cross-fade that still calls `onCovered()`. `velocity-skew`, `spotlight` and `edge-glow` don’t run under reduced motion.
|
|
36
|
+
|
|
10
37
|
## [5.2.0] - 2026-10-08
|
|
11
38
|
|
|
12
39
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var components_fx = require('./fx.cjs');
|
|
4
|
-
require('../chunks/base-B5i8qQPR.cjs');
|
|
4
|
+
var base = require('../chunks/base-B5i8qQPR.cjs');
|
|
5
5
|
require('../chunks/core-BDcszY4L.cjs');
|
|
6
6
|
require('./tokens.cjs');
|
|
7
7
|
require('../chunks/fx-B8hk1Fby.cjs');
|
|
@@ -439,6 +439,396 @@ const PHYSICS_FX = [
|
|
|
439
439
|
},
|
|
440
440
|
];
|
|
441
441
|
|
|
442
|
+
/** A full-viewport, aria-hidden, pointer-blocking (while covering) transition layer. */
|
|
443
|
+
function screen(css = '') {
|
|
444
|
+
const s = document.createElement('div');
|
|
445
|
+
s.setAttribute('aria-hidden', 'true');
|
|
446
|
+
s.setAttribute('data-usa-page-fx', '');
|
|
447
|
+
s.style.cssText = `position:fixed;inset:0;z-index:2147483001;pointer-events:auto;${css}`;
|
|
448
|
+
document.body.appendChild(s);
|
|
449
|
+
return s;
|
|
450
|
+
}
|
|
451
|
+
const wait = (a) => (a ? a.finished.catch(() => undefined) : Promise.resolve());
|
|
452
|
+
const call = async (fn) => {
|
|
453
|
+
if (typeof fn === 'function')
|
|
454
|
+
await fn();
|
|
455
|
+
};
|
|
456
|
+
/** Reduced-motion version of every transition: a quick cross-fade through the cover colour. */
|
|
457
|
+
async function crossFade(o, ctx) {
|
|
458
|
+
const s = screen(`background:${o.color};opacity:0`);
|
|
459
|
+
await wait(ctx.animate(s, [{ opacity: 0 }, { opacity: 1 }], { duration: 150, fill: 'forwards' }));
|
|
460
|
+
await call(o.onCovered);
|
|
461
|
+
await wait(ctx.animate(s, [{ opacity: 1 }, { opacity: 0 }], { duration: 150, fill: 'forwards' }));
|
|
462
|
+
s.remove();
|
|
463
|
+
}
|
|
464
|
+
/** Build a transition from a cover layer and cover / reveal animations of its parts. */
|
|
465
|
+
function transition(build) {
|
|
466
|
+
return async (el, o, ctx) => {
|
|
467
|
+
if (ctx.reduced)
|
|
468
|
+
return crossFade(o, ctx);
|
|
469
|
+
const s = screen('pointer-events:auto');
|
|
470
|
+
const { parts, cover, delay } = build(s, o, ctx, el);
|
|
471
|
+
const frames = (i) => (typeof cover === 'function' ? cover(i) : cover);
|
|
472
|
+
const half = o.duration / 2;
|
|
473
|
+
await Promise.all(parts.map((p, i) => wait(ctx.animate(p, frames(i), { duration: half, delay: delay?.(i) ?? 0, easing: 'cubic-bezier(0.65, 0, 0.35, 1)', fill: 'both' }))));
|
|
474
|
+
await call(o.onCovered);
|
|
475
|
+
if (o.hold)
|
|
476
|
+
await new Promise((r) => setTimeout(r, o.hold));
|
|
477
|
+
await Promise.all(parts.map((p, i) => wait(ctx.animate(p, [...frames(i)].reverse(), { duration: half, delay: delay?.(i) ?? 0, easing: 'cubic-bezier(0.65, 0, 0.35, 1)', fill: 'both' }))));
|
|
478
|
+
s.remove();
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const part = (s, css) => {
|
|
482
|
+
const p = document.createElement('div');
|
|
483
|
+
p.style.cssText = `position:absolute;${css}`;
|
|
484
|
+
s.appendChild(p);
|
|
485
|
+
return p;
|
|
486
|
+
};
|
|
487
|
+
const TRANSITION_DEFAULTS = { color: '#0f0f1a', duration: 1000, hold: 0, onCovered: undefined };
|
|
488
|
+
const PAGE_FX = [
|
|
489
|
+
{
|
|
490
|
+
name: 'curtain',
|
|
491
|
+
kind: 'page',
|
|
492
|
+
description: 'Two curtain panels close from the sides, call onCovered(), then open.',
|
|
493
|
+
reduced: 'run',
|
|
494
|
+
defaults: TRANSITION_DEFAULTS,
|
|
495
|
+
run: transition((s, o) => ({
|
|
496
|
+
parts: [part(s, `top:0;bottom:0;left:0;width:50.5%;background:${o.color}`), part(s, `top:0;bottom:0;right:0;width:50.5%;background:${o.color}`)],
|
|
497
|
+
cover: (i) => [{ transform: `translateX(${i ? 100 : -100}%)` }, { transform: 'translateX(0)' }],
|
|
498
|
+
})),
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'iris',
|
|
502
|
+
kind: 'page',
|
|
503
|
+
description: 'A circle closes on the click point (or the element’s center), calls onCovered(), then opens.',
|
|
504
|
+
reduced: 'run',
|
|
505
|
+
defaults: TRANSITION_DEFAULTS,
|
|
506
|
+
run: transition((s, o, ctx, el) => {
|
|
507
|
+
const { x, y } = origin(el, ctx);
|
|
508
|
+
const r = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) + 2;
|
|
509
|
+
// a huge box-shadow ring around a hole: the hole shrinks to 0 to cover
|
|
510
|
+
const p = part(s, `left:${x}px;top:${y}px;width:0;height:0;border-radius:50%;box-shadow:0 0 0 ${Math.ceil(r)}px ${o.color}`);
|
|
511
|
+
s.style.background = 'transparent';
|
|
512
|
+
return { parts: [p], cover: [{ width: `${r * 2}px`, height: `${r * 2}px`, margin: `${-r}px 0 0 ${-r}px` }, { width: '0px', height: '0px', margin: '0px 0 0 0px' }] };
|
|
513
|
+
}),
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'pixel-dissolve',
|
|
517
|
+
kind: 'page',
|
|
518
|
+
description: 'The screen fills with pixels in random order, calls onCovered(), then dissolves (`cols` × `rows`).',
|
|
519
|
+
reduced: 'run',
|
|
520
|
+
defaults: { ...TRANSITION_DEFAULTS, cols: 16, rows: 10 },
|
|
521
|
+
run: transition((s, o) => {
|
|
522
|
+
const n = o.cols * o.rows;
|
|
523
|
+
const order = Array.from({ length: n }, (_, i) => i).sort(() => Math.random() - 0.5);
|
|
524
|
+
const parts = Array.from({ length: n }, (_, i) => part(s, `left:${((i % o.cols) * 100) / o.cols}%;top:${(Math.floor(i / o.cols) * 100) / o.rows}%;width:${100 / o.cols + 0.2}%;height:${100 / o.rows + 0.2}%;background:${o.color};opacity:0`));
|
|
525
|
+
return { parts, cover: [{ opacity: 0 }, { opacity: 1 }], delay: (i) => (order[i] / n) * (o.duration / 2) * 0.6 };
|
|
526
|
+
}),
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: 'blinds',
|
|
530
|
+
kind: 'page',
|
|
531
|
+
description: 'Horizontal slats rotate shut like venetian blinds, call onCovered(), then open (`slats`).',
|
|
532
|
+
reduced: 'run',
|
|
533
|
+
defaults: { ...TRANSITION_DEFAULTS, slats: 8 },
|
|
534
|
+
run: transition((s, o) => ({
|
|
535
|
+
parts: Array.from({ length: o.slats }, (_, i) => part(s, `left:0;right:0;top:${(i * 100) / o.slats}%;height:${100 / o.slats + 0.3}%;background:${o.color};transform-origin:top`)),
|
|
536
|
+
cover: [{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }],
|
|
537
|
+
delay: (i) => i * 30,
|
|
538
|
+
})),
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: 'velocity-skew',
|
|
542
|
+
kind: 'scroll',
|
|
543
|
+
description: 'Skews the element with scroll velocity and eases back when scrolling stops (persistent).',
|
|
544
|
+
defaults: { max: 8, factor: 0.25 },
|
|
545
|
+
run: (el, o, ctx) => {
|
|
546
|
+
if (ctx.reduced)
|
|
547
|
+
return;
|
|
548
|
+
let last = scrollY;
|
|
549
|
+
let skew = 0;
|
|
550
|
+
let raf = 0;
|
|
551
|
+
const tick = () => {
|
|
552
|
+
const y = scrollY;
|
|
553
|
+
const target = Math.max(-o.max, Math.min(o.max, (y - last) * o.factor));
|
|
554
|
+
last = y;
|
|
555
|
+
skew += (target - skew) * 0.2;
|
|
556
|
+
el.style.transform = `skewY(${skew.toFixed(3)}deg)`;
|
|
557
|
+
raf = Math.abs(skew) > 0.01 || Math.abs(target) > 0.01 ? requestAnimationFrame(tick) : 0;
|
|
558
|
+
if (!raf)
|
|
559
|
+
el.style.transform = '';
|
|
560
|
+
};
|
|
561
|
+
const onScroll = () => {
|
|
562
|
+
if (!raf)
|
|
563
|
+
raf = requestAnimationFrame(tick);
|
|
564
|
+
};
|
|
565
|
+
addEventListener('scroll', onScroll, { passive: true });
|
|
566
|
+
return () => {
|
|
567
|
+
removeEventListener('scroll', onScroll);
|
|
568
|
+
cancelAnimationFrame(raf);
|
|
569
|
+
el.style.transform = '';
|
|
570
|
+
};
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'spotlight',
|
|
575
|
+
kind: 'cursor',
|
|
576
|
+
description: 'Dims the page except a soft circle that follows the pointer (persistent; `radius`, `dim`).',
|
|
577
|
+
defaults: { radius: 180, dim: 0.72 },
|
|
578
|
+
run: (_el, o, ctx) => {
|
|
579
|
+
if (ctx.reduced)
|
|
580
|
+
return;
|
|
581
|
+
const s = screen(`pointer-events:none;background:radial-gradient(circle ${o.radius}px at var(--x,50%) var(--y,50%),transparent 0,transparent 60%,rgba(0,0,0,${o.dim}) 100%)`);
|
|
582
|
+
const move = (e) => {
|
|
583
|
+
s.style.setProperty('--x', `${e.clientX}px`);
|
|
584
|
+
s.style.setProperty('--y', `${e.clientY}px`);
|
|
585
|
+
};
|
|
586
|
+
addEventListener('pointermove', move, { passive: true });
|
|
587
|
+
return () => {
|
|
588
|
+
removeEventListener('pointermove', move);
|
|
589
|
+
s.remove();
|
|
590
|
+
};
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: 'edge-glow',
|
|
595
|
+
kind: 'scroll',
|
|
596
|
+
description: 'A soft glow lights the top / bottom edge of the viewport while scrolling in that direction (persistent).',
|
|
597
|
+
defaults: { color: '#7c5cff', size: 90 },
|
|
598
|
+
run: (_el, o, ctx) => {
|
|
599
|
+
if (ctx.reduced)
|
|
600
|
+
return;
|
|
601
|
+
const s = screen('pointer-events:none;opacity:0;transition:opacity .35s ease-out');
|
|
602
|
+
let last = scrollY;
|
|
603
|
+
let t = 0;
|
|
604
|
+
const onScroll = () => {
|
|
605
|
+
const down = scrollY >= last;
|
|
606
|
+
last = scrollY;
|
|
607
|
+
s.style.background = `linear-gradient(${down ? 'to top' : 'to bottom'},${o.color}55,transparent ${o.size}px)`;
|
|
608
|
+
s.style.opacity = '1';
|
|
609
|
+
clearTimeout(t);
|
|
610
|
+
t = setTimeout(() => (s.style.opacity = '0'), 160);
|
|
611
|
+
};
|
|
612
|
+
addEventListener('scroll', onScroll, { passive: true });
|
|
613
|
+
return () => {
|
|
614
|
+
removeEventListener('scroll', onScroll);
|
|
615
|
+
clearTimeout(t);
|
|
616
|
+
s.remove();
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 5.4 — `<usa-story template="…">` scroll-storytelling templates.
|
|
624
|
+
*
|
|
625
|
+
* - `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the
|
|
626
|
+
* step in view gets `data-active`, the stage gets `data-active-step="<index>"`.
|
|
627
|
+
* - `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
|
|
628
|
+
* - `zoom` — the `[data-stage]` zooms toward the viewer (`zoom="6"`) and fades.
|
|
629
|
+
* - `compare` — before / after (`[data-before]`, `[data-after]`) wipe driven by
|
|
630
|
+
* scroll, plus a draggable, keyboard-accessible handle (`role="slider"`).
|
|
631
|
+
* - `counter` — `[data-count="1234"]` numbers count up when they enter view.
|
|
632
|
+
* - `highlight` — paragraphs dim except the one crossing the viewport center.
|
|
633
|
+
*
|
|
634
|
+
* Every template sets `--usa-story-progress` (0–1) on the host and dispatches
|
|
635
|
+
* `usa-story-step` (`detail: { index }`). Reduced motion: no sliding / zooming
|
|
636
|
+
* (the gallery stacks vertically), counters show final values, the rest is
|
|
637
|
+
* class changes only.
|
|
638
|
+
*/
|
|
639
|
+
const STORY_TEMPLATES = ['pin', 'gallery', 'zoom', 'compare', 'counter', 'highlight'];
|
|
640
|
+
const CSS = `usa-story{display:block;position:relative}
|
|
641
|
+
usa-story[template=pin] [data-stage],usa-story[template=gallery] [data-sticky],usa-story[template=zoom] [data-stage],usa-story[template=compare] [data-sticky]{position:sticky;top:0}
|
|
642
|
+
usa-story[template=pin] [data-stage]{align-self:start}
|
|
643
|
+
usa-story[template=pin] [data-step]{min-height:80vh;opacity:.35;transition:opacity .3s}
|
|
644
|
+
usa-story[template=pin] [data-step][data-active]{opacity:1}
|
|
645
|
+
usa-story[template=gallery] [data-sticky]{height:100vh;overflow:hidden;display:flex;align-items:center}
|
|
646
|
+
usa-story[template=gallery] [data-track]{display:flex;gap:24px;will-change:transform}
|
|
647
|
+
usa-story[template=gallery][data-static] [data-sticky]{position:static;height:auto;overflow:visible}
|
|
648
|
+
usa-story[template=gallery][data-static] [data-track]{flex-direction:column;transform:none!important}
|
|
649
|
+
usa-story[template=zoom] [data-stage]{height:100vh;overflow:hidden;display:grid;place-items:center}
|
|
650
|
+
usa-story[template=compare] [data-sticky]{height:var(--usa-story-h,70vh);overflow:hidden}
|
|
651
|
+
usa-story[template=compare] [data-before],usa-story[template=compare] [data-after]{position:absolute;inset:0}
|
|
652
|
+
usa-story[template=compare] [data-after]{clip-path:inset(0 0 0 var(--usa-split,50%))}
|
|
653
|
+
usa-story [data-handle]{position:absolute;top:0;bottom:0;left:var(--usa-split,50%);width:4px;margin-left:-2px;background:#fff;box-shadow:0 0 0 1px #0003;cursor:ew-resize;touch-action:none}
|
|
654
|
+
usa-story [data-handle]:focus-visible{outline:3px solid #7c5cff;outline-offset:2px}
|
|
655
|
+
usa-story[template=highlight] p,usa-story[template=highlight] [data-step]{opacity:.3;transition:opacity .25s}
|
|
656
|
+
usa-story[template=highlight] [data-active]{opacity:1}`;
|
|
657
|
+
const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
|
|
658
|
+
/** Progress of `el` through the viewport: 0 when its top hits the viewport top, 1 when its bottom hits the viewport bottom. */
|
|
659
|
+
function storyProgress(el, vh = typeof innerHeight === 'number' ? innerHeight : 800) {
|
|
660
|
+
const r = el.getBoundingClientRect();
|
|
661
|
+
const span = r.height - vh;
|
|
662
|
+
return span > 0 ? clamp(-r.top / span) : clamp((vh - r.top) / (vh + r.height));
|
|
663
|
+
}
|
|
664
|
+
/** Format a counted value like the target (`1,234`, `12.5`, prefix / suffix kept). */
|
|
665
|
+
function formatCount(target, t) {
|
|
666
|
+
const m = target.match(/^(\D*)([\d,]*\.?\d+)(.*)$/);
|
|
667
|
+
if (!m)
|
|
668
|
+
return target;
|
|
669
|
+
const [, pre, num, post] = m;
|
|
670
|
+
const n = parseFloat(num.replace(/,/g, ''));
|
|
671
|
+
const dec = (num.split('.')[1] || '').length;
|
|
672
|
+
let s = (n * t).toFixed(dec);
|
|
673
|
+
if (num.includes(','))
|
|
674
|
+
s = Number(s).toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec });
|
|
675
|
+
return pre + s + post;
|
|
676
|
+
}
|
|
677
|
+
function defineStory(tag = 'usa-story') {
|
|
678
|
+
return base.defineElement(tag, (Base) => class UsaStory extends Base {
|
|
679
|
+
constructor() {
|
|
680
|
+
super(...arguments);
|
|
681
|
+
this.progress = 0;
|
|
682
|
+
this.step = -1;
|
|
683
|
+
this.frame = 0;
|
|
684
|
+
}
|
|
685
|
+
static get observedAttributes() {
|
|
686
|
+
return ['template', 'zoom'];
|
|
687
|
+
}
|
|
688
|
+
get template() {
|
|
689
|
+
const t = this.str('template', 'pin');
|
|
690
|
+
return STORY_TEMPLATES.includes(t) ? t : 'pin';
|
|
691
|
+
}
|
|
692
|
+
steps() {
|
|
693
|
+
const own = Array.from(this.querySelectorAll('[data-step]:not([data-stage])'));
|
|
694
|
+
return own.length || this.template !== 'highlight' ? own : Array.from(this.querySelectorAll('p'));
|
|
695
|
+
}
|
|
696
|
+
setStep(i) {
|
|
697
|
+
if (i === this.step)
|
|
698
|
+
return;
|
|
699
|
+
this.step = i;
|
|
700
|
+
this.steps().forEach((s, k) => s.toggleAttribute('data-active', k === i));
|
|
701
|
+
const stage = this.querySelector('[data-stage]');
|
|
702
|
+
if (stage)
|
|
703
|
+
stage.dataset.activeStep = String(i);
|
|
704
|
+
this.dispatchEvent(new CustomEvent('usa-story-step', { detail: { index: i }, bubbles: true }));
|
|
705
|
+
}
|
|
706
|
+
/** Recompute from the current scroll position (called on scroll / resize). */
|
|
707
|
+
update() {
|
|
708
|
+
this.frame = 0;
|
|
709
|
+
const p = (this.progress = storyProgress(this));
|
|
710
|
+
this.style.setProperty('--usa-story-progress', p.toFixed(4));
|
|
711
|
+
const t = this.template;
|
|
712
|
+
const vh = innerHeight || 800;
|
|
713
|
+
if (t === 'pin' || t === 'highlight') {
|
|
714
|
+
const mid = vh / 2;
|
|
715
|
+
let best = -1;
|
|
716
|
+
let bestD = Infinity;
|
|
717
|
+
this.steps().forEach((s, i) => {
|
|
718
|
+
const r = s.getBoundingClientRect();
|
|
719
|
+
const d = r.top <= mid && r.bottom >= mid ? 0 : Math.min(Math.abs(r.top - mid), Math.abs(r.bottom - mid));
|
|
720
|
+
if (d < bestD)
|
|
721
|
+
(bestD = d), (best = i);
|
|
722
|
+
});
|
|
723
|
+
this.setStep(best);
|
|
724
|
+
}
|
|
725
|
+
else if (t === 'gallery' && !this.reduced) {
|
|
726
|
+
const track = this.querySelector('[data-track]');
|
|
727
|
+
const box = this.querySelector('[data-sticky]');
|
|
728
|
+
if (track && box)
|
|
729
|
+
track.style.transform = `translateX(${(-p * Math.max(0, track.scrollWidth - box.clientWidth)).toFixed(1)}px)`;
|
|
730
|
+
}
|
|
731
|
+
else if (t === 'zoom' && !this.reduced) {
|
|
732
|
+
const inner = this.querySelector('[data-stage] > *');
|
|
733
|
+
if (inner) {
|
|
734
|
+
inner.style.transform = `scale(${(1 + p * (this.num('zoom', 6) - 1)).toFixed(3)})`;
|
|
735
|
+
inner.style.opacity = String(clamp(1.4 - p * 1.4).toFixed(3));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else if (t === 'compare' && !this.hasAttribute('data-dragged')) {
|
|
739
|
+
this.setSplit(p * 100);
|
|
740
|
+
}
|
|
741
|
+
else if (t === 'counter') {
|
|
742
|
+
this.querySelectorAll('[data-count]:not([data-counted])').forEach((el) => {
|
|
743
|
+
const r = el.getBoundingClientRect();
|
|
744
|
+
if (r.top < vh * 0.9 && r.bottom > 0)
|
|
745
|
+
this.count(el);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
setSplit(pct) {
|
|
750
|
+
const v = clamp(pct, 0, 100);
|
|
751
|
+
this.style.setProperty('--usa-split', `${v.toFixed(2)}%`);
|
|
752
|
+
const h = this.querySelector('[data-handle]');
|
|
753
|
+
if (h)
|
|
754
|
+
h.setAttribute('aria-valuenow', String(Math.round(v)));
|
|
755
|
+
}
|
|
756
|
+
count(el) {
|
|
757
|
+
el.dataset.counted = '';
|
|
758
|
+
const target = el.dataset.count || el.textContent || '0';
|
|
759
|
+
el.setAttribute('aria-label', target);
|
|
760
|
+
if (this.reduced) {
|
|
761
|
+
el.textContent = target;
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const dur = Number(el.dataset.duration) || 1600;
|
|
765
|
+
const t0 = performance.now();
|
|
766
|
+
const tick = (now) => {
|
|
767
|
+
const k = clamp((now - t0) / dur);
|
|
768
|
+
el.textContent = formatCount(target, 1 - (1 - k) ** 3);
|
|
769
|
+
if (k < 1)
|
|
770
|
+
requestAnimationFrame(tick);
|
|
771
|
+
};
|
|
772
|
+
requestAnimationFrame(tick);
|
|
773
|
+
}
|
|
774
|
+
mount() {
|
|
775
|
+
this.toggleAttribute('data-static', this.reduced);
|
|
776
|
+
if (this.template === 'compare')
|
|
777
|
+
this.mountCompare();
|
|
778
|
+
const kick = () => {
|
|
779
|
+
if (!this.frame)
|
|
780
|
+
this.frame = requestAnimationFrame(() => this.update());
|
|
781
|
+
};
|
|
782
|
+
this.listen(window, 'scroll', kick, { passive: true });
|
|
783
|
+
this.listen(window, 'resize', kick);
|
|
784
|
+
this.onCleanup(() => cancelAnimationFrame(this.frame));
|
|
785
|
+
this.update();
|
|
786
|
+
}
|
|
787
|
+
mountCompare() {
|
|
788
|
+
const box = this.querySelector('[data-sticky]') || this;
|
|
789
|
+
let h = this.querySelector('[data-handle]');
|
|
790
|
+
if (!h) {
|
|
791
|
+
h = document.createElement('div');
|
|
792
|
+
h.setAttribute('data-handle', '');
|
|
793
|
+
box.appendChild(h);
|
|
794
|
+
this.onCleanup(() => h.remove());
|
|
795
|
+
}
|
|
796
|
+
h.tabIndex = 0;
|
|
797
|
+
h.setAttribute('role', 'slider');
|
|
798
|
+
h.setAttribute('aria-label', this.str('label', 'Before / after'));
|
|
799
|
+
h.setAttribute('aria-valuemin', '0');
|
|
800
|
+
h.setAttribute('aria-valuemax', '100');
|
|
801
|
+
const at = (e) => {
|
|
802
|
+
const r = box.getBoundingClientRect();
|
|
803
|
+
this.dataset.dragged = '';
|
|
804
|
+
this.setSplit(((e.clientX - r.left) / (r.width || 1)) * 100);
|
|
805
|
+
};
|
|
806
|
+
let drag = false;
|
|
807
|
+
this.listen(h, 'pointerdown', (e) => {
|
|
808
|
+
drag = true;
|
|
809
|
+
h.setPointerCapture?.(e.pointerId);
|
|
810
|
+
});
|
|
811
|
+
this.listen(h, 'pointermove', (e) => drag && at(e));
|
|
812
|
+
this.listen(h, 'pointerup', () => (drag = false));
|
|
813
|
+
this.listen(h, 'keydown', (e) => {
|
|
814
|
+
const cur = parseFloat(h.getAttribute('aria-valuenow') || '50');
|
|
815
|
+
const step = e.shiftKey ? 10 : 2;
|
|
816
|
+
const next = e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? cur - step : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? cur + step : e.key === 'Home' ? 0 : e.key === 'End' ? 100 : null;
|
|
817
|
+
if (next == null)
|
|
818
|
+
return;
|
|
819
|
+
e.preventDefault();
|
|
820
|
+
this.dataset.dragged = '';
|
|
821
|
+
this.setSplit(next);
|
|
822
|
+
});
|
|
823
|
+
this.setSplit(50);
|
|
824
|
+
}
|
|
825
|
+
unmount() {
|
|
826
|
+
this.step = -1;
|
|
827
|
+
delete this.dataset.dragged;
|
|
828
|
+
}
|
|
829
|
+
}, { id: 'usa-story', text: CSS });
|
|
830
|
+
}
|
|
831
|
+
|
|
442
832
|
/**
|
|
443
833
|
* use-scroll-animate/components/effects — the 5.x effect packs, all
|
|
444
834
|
* registered through `registerEffect()` (5.0) and playable with
|
|
@@ -455,6 +845,7 @@ const PHYSICS_FX = [
|
|
|
455
845
|
const EFFECT_PACKS = {
|
|
456
846
|
'cards-click': [...CARD_FX, ...CLICK_FX],
|
|
457
847
|
physics: PHYSICS_FX,
|
|
848
|
+
page: PAGE_FX,
|
|
458
849
|
};
|
|
459
850
|
/** 5.1: card & click effects 2.0. */
|
|
460
851
|
function registerCardClickEffects() {
|
|
@@ -464,6 +855,14 @@ function registerCardClickEffects() {
|
|
|
464
855
|
function registerPhysicsEffects() {
|
|
465
856
|
components_fx.registerEffects(EFFECT_PACKS.physics);
|
|
466
857
|
}
|
|
858
|
+
/** 5.3: page-wide transitions and effects. */
|
|
859
|
+
function registerPageEffects() {
|
|
860
|
+
components_fx.registerEffects(EFFECT_PACKS.page);
|
|
861
|
+
}
|
|
862
|
+
/** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
|
|
863
|
+
function defineEffectElements() {
|
|
864
|
+
defineStory();
|
|
865
|
+
}
|
|
467
866
|
/** Register the built-ins and every pack (idempotent). */
|
|
468
867
|
function registerAllEffects() {
|
|
469
868
|
components_fx.registerBuiltinEffects();
|
|
@@ -474,12 +873,19 @@ function registerAllEffects() {
|
|
|
474
873
|
exports.CARD_FX = CARD_FX;
|
|
475
874
|
exports.CLICK_FX = CLICK_FX;
|
|
476
875
|
exports.EFFECT_PACKS = EFFECT_PACKS;
|
|
876
|
+
exports.PAGE_FX = PAGE_FX;
|
|
477
877
|
exports.PHYSICS_FX = PHYSICS_FX;
|
|
878
|
+
exports.STORY_TEMPLATES = STORY_TEMPLATES;
|
|
478
879
|
exports.bounceKeyframes = bounceKeyframes;
|
|
880
|
+
exports.defineEffectElements = defineEffectElements;
|
|
881
|
+
exports.defineStory = defineStory;
|
|
882
|
+
exports.formatCount = formatCount;
|
|
479
883
|
exports.fxLayer = fxLayer;
|
|
480
884
|
exports.registerAllEffects = registerAllEffects;
|
|
481
885
|
exports.registerCardClickEffects = registerCardClickEffects;
|
|
886
|
+
exports.registerPageEffects = registerPageEffects;
|
|
482
887
|
exports.registerPhysicsEffects = registerPhysicsEffects;
|
|
483
888
|
exports.solveSpring = solveSpring;
|
|
484
889
|
exports.springKeyframes = springKeyframes;
|
|
890
|
+
exports.storyProgress = storyProgress;
|
|
485
891
|
//# sourceMappingURL=effects.cjs.map
|