use-scroll-animate 5.3.0 → 5.5.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 +26 -0
- package/dist/components/effects.cjs +516 -1
- package/dist/components/effects.cjs.map +1 -1
- package/dist/components/effects.d.cts +93 -2
- package/dist/components/effects.d.ts +93 -2
- package/dist/components/effects.js +507 -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
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
type MotionSensitivity = 'full' | 'gentle' | 'minimal' | 'static';
|
|
2
2
|
type Cleanup = () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Members shared by every `<usa-*>` element. Attribute helpers, a cleanup
|
|
5
|
+
* bag that is emptied on disconnect, and motion helpers that degrade to the
|
|
6
|
+
* final state without WAAPI or under reduced motion.
|
|
7
|
+
*/
|
|
8
|
+
interface UsaElement extends HTMLElement {
|
|
9
|
+
/** `true` while reduced motion applies to this element. */
|
|
10
|
+
readonly reduced: boolean;
|
|
11
|
+
}
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* 5.0 — unified plugin-style effect registration. Every effect (built-in or
|
|
@@ -97,6 +106,84 @@ declare const PHYSICS_FX: EffectDefinition[];
|
|
|
97
106
|
|
|
98
107
|
declare const PAGE_FX: EffectDefinition[];
|
|
99
108
|
|
|
109
|
+
/**
|
|
110
|
+
* 5.5 — generative backgrounds on Canvas 2D, registered through
|
|
111
|
+
* `registerEffect()` (kind `background`): `flow-field`, `voronoi`,
|
|
112
|
+
* `mesh-gradient`, `starfield`, `metaballs`, `contours`.
|
|
113
|
+
*
|
|
114
|
+
* Shared runner (`canvasBackground()`): a canvas behind the element's content
|
|
115
|
+
* (`aria-hidden`, pointer-transparent), rendering only while visible
|
|
116
|
+
* (IntersectionObserver) and the tab is shown, with adaptive quality — the
|
|
117
|
+
* render scale drops when frames get slow and recovers when they are fast.
|
|
118
|
+
* Reduced motion: one static frame, no loop.
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
interface GenFrame {
|
|
122
|
+
ctx: CanvasRenderingContext2D;
|
|
123
|
+
/** CSS-pixel size of the canvas. */
|
|
124
|
+
w: number;
|
|
125
|
+
h: number;
|
|
126
|
+
/** Seconds since start (0 for the static reduced-motion frame). */
|
|
127
|
+
t: number;
|
|
128
|
+
/** Render scale 0.35–1 (adaptive quality). */
|
|
129
|
+
quality: number;
|
|
130
|
+
/** Per-effect state, created by `init`. */
|
|
131
|
+
state: any;
|
|
132
|
+
/** Merged options. */
|
|
133
|
+
o: any;
|
|
134
|
+
}
|
|
135
|
+
interface GenerativeSpec {
|
|
136
|
+
init?: (w: number, h: number, o: any) => any;
|
|
137
|
+
draw: (f: GenFrame) => void;
|
|
138
|
+
}
|
|
139
|
+
/** Deterministic smooth pseudo-noise in [-1, 1] (sum of sines — cheap, no tables). */
|
|
140
|
+
declare function noise2(x: number, y: number, t?: number): number;
|
|
141
|
+
/** Hex `#rrggbb` → [r, g, b]. */
|
|
142
|
+
declare function hexRgb(hex: string): [number, number, number];
|
|
143
|
+
/**
|
|
144
|
+
* Mount a generative canvas behind `el` and run `spec` on it. Returns the
|
|
145
|
+
* cleanup. Exposed for custom generative effects.
|
|
146
|
+
*/
|
|
147
|
+
declare function canvasBackground(el: HTMLElement, fx: EffectContext, spec: GenerativeSpec, o: any): () => void;
|
|
148
|
+
declare const GENERATIVE_FX: EffectDefinition[];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 5.4 — `<usa-story template="…">` scroll-storytelling templates.
|
|
152
|
+
*
|
|
153
|
+
* - `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the
|
|
154
|
+
* step in view gets `data-active`, the stage gets `data-active-step="<index>"`.
|
|
155
|
+
* - `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
|
|
156
|
+
* - `zoom` — the `[data-stage]` zooms toward the viewer (`zoom="6"`) and fades.
|
|
157
|
+
* - `compare` — before / after (`[data-before]`, `[data-after]`) wipe driven by
|
|
158
|
+
* scroll, plus a draggable, keyboard-accessible handle (`role="slider"`).
|
|
159
|
+
* - `counter` — `[data-count="1234"]` numbers count up when they enter view.
|
|
160
|
+
* - `highlight` — paragraphs dim except the one crossing the viewport center.
|
|
161
|
+
*
|
|
162
|
+
* Every template sets `--usa-story-progress` (0–1) on the host and dispatches
|
|
163
|
+
* `usa-story-step` (`detail: { index }`). Reduced motion: no sliding / zooming
|
|
164
|
+
* (the gallery stacks vertically), counters show final values, the rest is
|
|
165
|
+
* class changes only.
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
declare const STORY_TEMPLATES: readonly ["pin", "gallery", "zoom", "compare", "counter", "highlight"];
|
|
169
|
+
type StoryTemplate = (typeof STORY_TEMPLATES)[number];
|
|
170
|
+
interface UsaStoryElement extends UsaElement {
|
|
171
|
+
/** Scroll progress through the story, 0–1. */
|
|
172
|
+
readonly progress: number;
|
|
173
|
+
/** Index of the active step (pin / highlight), or -1. */
|
|
174
|
+
readonly step: number;
|
|
175
|
+
}
|
|
176
|
+
/** Progress of `el` through the viewport: 0 when its top hits the viewport top, 1 when its bottom hits the viewport bottom. */
|
|
177
|
+
declare function storyProgress(el: Element, vh?: number): number;
|
|
178
|
+
/** Format a counted value like the target (`1,234`, `12.5`, prefix / suffix kept). */
|
|
179
|
+
declare function formatCount(target: string, t: number): string;
|
|
180
|
+
declare function defineStory(tag?: string): CustomElementConstructor | undefined;
|
|
181
|
+
declare global {
|
|
182
|
+
interface HTMLElementTagNameMap {
|
|
183
|
+
'usa-story': UsaStoryElement;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
100
187
|
/** Helpers shared by the 5.x effect packs. */
|
|
101
188
|
|
|
102
189
|
/** A fixed, pointer-transparent, aria-hidden layer for transient particles. */
|
|
@@ -110,8 +197,12 @@ declare function registerCardClickEffects(): void;
|
|
|
110
197
|
declare function registerPhysicsEffects(): void;
|
|
111
198
|
/** 5.3: page-wide transitions and effects. */
|
|
112
199
|
declare function registerPageEffects(): void;
|
|
200
|
+
/** 5.5: generative Canvas 2D backgrounds. */
|
|
201
|
+
declare function registerGenerativeEffects(): void;
|
|
202
|
+
/** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
|
|
203
|
+
declare function defineEffectElements(): void;
|
|
113
204
|
/** Register the built-ins and every pack (idempotent). */
|
|
114
205
|
declare function registerAllEffects(): void;
|
|
115
206
|
|
|
116
|
-
export { CARD_FX, CLICK_FX, EFFECT_PACKS, PAGE_FX, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes };
|
|
117
|
-
export type { SpringOptions };
|
|
207
|
+
export { CARD_FX, CLICK_FX, EFFECT_PACKS, GENERATIVE_FX, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, bounceKeyframes, canvasBackground, defineEffectElements, defineStory, formatCount, fxLayer, hexRgb, noise2, registerAllEffects, registerCardClickEffects, registerGenerativeEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
|
|
208
|
+
export type { GenFrame, GenerativeSpec, SpringOptions, StoryTemplate, UsaStoryElement };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerBuiltinEffects, registerEffects } from './fx.js';
|
|
2
|
-
import '../chunks/base-C3Sw9sAO.js';
|
|
2
|
+
import { x as defineElement } from '../chunks/base-C3Sw9sAO.js';
|
|
3
3
|
import '../chunks/core-DN3hHbHh.js';
|
|
4
4
|
import './tokens.js';
|
|
5
5
|
import '../chunks/fx-ChjxrMBo.js';
|
|
@@ -617,6 +617,502 @@ const PAGE_FX = [
|
|
|
617
617
|
},
|
|
618
618
|
];
|
|
619
619
|
|
|
620
|
+
/**
|
|
621
|
+
* 5.4 — `<usa-story template="…">` scroll-storytelling templates.
|
|
622
|
+
*
|
|
623
|
+
* - `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the
|
|
624
|
+
* step in view gets `data-active`, the stage gets `data-active-step="<index>"`.
|
|
625
|
+
* - `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
|
|
626
|
+
* - `zoom` — the `[data-stage]` zooms toward the viewer (`zoom="6"`) and fades.
|
|
627
|
+
* - `compare` — before / after (`[data-before]`, `[data-after]`) wipe driven by
|
|
628
|
+
* scroll, plus a draggable, keyboard-accessible handle (`role="slider"`).
|
|
629
|
+
* - `counter` — `[data-count="1234"]` numbers count up when they enter view.
|
|
630
|
+
* - `highlight` — paragraphs dim except the one crossing the viewport center.
|
|
631
|
+
*
|
|
632
|
+
* Every template sets `--usa-story-progress` (0–1) on the host and dispatches
|
|
633
|
+
* `usa-story-step` (`detail: { index }`). Reduced motion: no sliding / zooming
|
|
634
|
+
* (the gallery stacks vertically), counters show final values, the rest is
|
|
635
|
+
* class changes only.
|
|
636
|
+
*/
|
|
637
|
+
const STORY_TEMPLATES = ['pin', 'gallery', 'zoom', 'compare', 'counter', 'highlight'];
|
|
638
|
+
const CSS = `usa-story{display:block;position:relative}
|
|
639
|
+
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}
|
|
640
|
+
usa-story[template=pin] [data-stage]{align-self:start}
|
|
641
|
+
usa-story[template=pin] [data-step]{min-height:80vh;opacity:.35;transition:opacity .3s}
|
|
642
|
+
usa-story[template=pin] [data-step][data-active]{opacity:1}
|
|
643
|
+
usa-story[template=gallery] [data-sticky]{height:100vh;overflow:hidden;display:flex;align-items:center}
|
|
644
|
+
usa-story[template=gallery] [data-track]{display:flex;gap:24px;will-change:transform}
|
|
645
|
+
usa-story[template=gallery][data-static] [data-sticky]{position:static;height:auto;overflow:visible}
|
|
646
|
+
usa-story[template=gallery][data-static] [data-track]{flex-direction:column;transform:none!important}
|
|
647
|
+
usa-story[template=zoom] [data-stage]{height:100vh;overflow:hidden;display:grid;place-items:center}
|
|
648
|
+
usa-story[template=compare] [data-sticky]{height:var(--usa-story-h,70vh);overflow:hidden}
|
|
649
|
+
usa-story[template=compare] [data-before],usa-story[template=compare] [data-after]{position:absolute;inset:0}
|
|
650
|
+
usa-story[template=compare] [data-after]{clip-path:inset(0 0 0 var(--usa-split,50%))}
|
|
651
|
+
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}
|
|
652
|
+
usa-story [data-handle]:focus-visible{outline:3px solid #7c5cff;outline-offset:2px}
|
|
653
|
+
usa-story[template=highlight] p,usa-story[template=highlight] [data-step]{opacity:.3;transition:opacity .25s}
|
|
654
|
+
usa-story[template=highlight] [data-active]{opacity:1}`;
|
|
655
|
+
const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
|
|
656
|
+
/** Progress of `el` through the viewport: 0 when its top hits the viewport top, 1 when its bottom hits the viewport bottom. */
|
|
657
|
+
function storyProgress(el, vh = typeof innerHeight === 'number' ? innerHeight : 800) {
|
|
658
|
+
const r = el.getBoundingClientRect();
|
|
659
|
+
const span = r.height - vh;
|
|
660
|
+
return span > 0 ? clamp(-r.top / span) : clamp((vh - r.top) / (vh + r.height));
|
|
661
|
+
}
|
|
662
|
+
/** Format a counted value like the target (`1,234`, `12.5`, prefix / suffix kept). */
|
|
663
|
+
function formatCount(target, t) {
|
|
664
|
+
const m = target.match(/^(\D*)([\d,]*\.?\d+)(.*)$/);
|
|
665
|
+
if (!m)
|
|
666
|
+
return target;
|
|
667
|
+
const [, pre, num, post] = m;
|
|
668
|
+
const n = parseFloat(num.replace(/,/g, ''));
|
|
669
|
+
const dec = (num.split('.')[1] || '').length;
|
|
670
|
+
let s = (n * t).toFixed(dec);
|
|
671
|
+
if (num.includes(','))
|
|
672
|
+
s = Number(s).toLocaleString('en-US', { minimumFractionDigits: dec, maximumFractionDigits: dec });
|
|
673
|
+
return pre + s + post;
|
|
674
|
+
}
|
|
675
|
+
function defineStory(tag = 'usa-story') {
|
|
676
|
+
return defineElement(tag, (Base) => class UsaStory extends Base {
|
|
677
|
+
constructor() {
|
|
678
|
+
super(...arguments);
|
|
679
|
+
this.progress = 0;
|
|
680
|
+
this.step = -1;
|
|
681
|
+
this.frame = 0;
|
|
682
|
+
}
|
|
683
|
+
static get observedAttributes() {
|
|
684
|
+
return ['template', 'zoom'];
|
|
685
|
+
}
|
|
686
|
+
get template() {
|
|
687
|
+
const t = this.str('template', 'pin');
|
|
688
|
+
return STORY_TEMPLATES.includes(t) ? t : 'pin';
|
|
689
|
+
}
|
|
690
|
+
steps() {
|
|
691
|
+
const own = Array.from(this.querySelectorAll('[data-step]:not([data-stage])'));
|
|
692
|
+
return own.length || this.template !== 'highlight' ? own : Array.from(this.querySelectorAll('p'));
|
|
693
|
+
}
|
|
694
|
+
setStep(i) {
|
|
695
|
+
if (i === this.step)
|
|
696
|
+
return;
|
|
697
|
+
this.step = i;
|
|
698
|
+
this.steps().forEach((s, k) => s.toggleAttribute('data-active', k === i));
|
|
699
|
+
const stage = this.querySelector('[data-stage]');
|
|
700
|
+
if (stage)
|
|
701
|
+
stage.dataset.activeStep = String(i);
|
|
702
|
+
this.dispatchEvent(new CustomEvent('usa-story-step', { detail: { index: i }, bubbles: true }));
|
|
703
|
+
}
|
|
704
|
+
/** Recompute from the current scroll position (called on scroll / resize). */
|
|
705
|
+
update() {
|
|
706
|
+
this.frame = 0;
|
|
707
|
+
const p = (this.progress = storyProgress(this));
|
|
708
|
+
this.style.setProperty('--usa-story-progress', p.toFixed(4));
|
|
709
|
+
const t = this.template;
|
|
710
|
+
const vh = innerHeight || 800;
|
|
711
|
+
if (t === 'pin' || t === 'highlight') {
|
|
712
|
+
const mid = vh / 2;
|
|
713
|
+
let best = -1;
|
|
714
|
+
let bestD = Infinity;
|
|
715
|
+
this.steps().forEach((s, i) => {
|
|
716
|
+
const r = s.getBoundingClientRect();
|
|
717
|
+
const d = r.top <= mid && r.bottom >= mid ? 0 : Math.min(Math.abs(r.top - mid), Math.abs(r.bottom - mid));
|
|
718
|
+
if (d < bestD)
|
|
719
|
+
(bestD = d), (best = i);
|
|
720
|
+
});
|
|
721
|
+
this.setStep(best);
|
|
722
|
+
}
|
|
723
|
+
else if (t === 'gallery' && !this.reduced) {
|
|
724
|
+
const track = this.querySelector('[data-track]');
|
|
725
|
+
const box = this.querySelector('[data-sticky]');
|
|
726
|
+
if (track && box)
|
|
727
|
+
track.style.transform = `translateX(${(-p * Math.max(0, track.scrollWidth - box.clientWidth)).toFixed(1)}px)`;
|
|
728
|
+
}
|
|
729
|
+
else if (t === 'zoom' && !this.reduced) {
|
|
730
|
+
const inner = this.querySelector('[data-stage] > *');
|
|
731
|
+
if (inner) {
|
|
732
|
+
inner.style.transform = `scale(${(1 + p * (this.num('zoom', 6) - 1)).toFixed(3)})`;
|
|
733
|
+
inner.style.opacity = String(clamp(1.4 - p * 1.4).toFixed(3));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else if (t === 'compare' && !this.hasAttribute('data-dragged')) {
|
|
737
|
+
this.setSplit(p * 100);
|
|
738
|
+
}
|
|
739
|
+
else if (t === 'counter') {
|
|
740
|
+
this.querySelectorAll('[data-count]:not([data-counted])').forEach((el) => {
|
|
741
|
+
const r = el.getBoundingClientRect();
|
|
742
|
+
if (r.top < vh * 0.9 && r.bottom > 0)
|
|
743
|
+
this.count(el);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
setSplit(pct) {
|
|
748
|
+
const v = clamp(pct, 0, 100);
|
|
749
|
+
this.style.setProperty('--usa-split', `${v.toFixed(2)}%`);
|
|
750
|
+
const h = this.querySelector('[data-handle]');
|
|
751
|
+
if (h)
|
|
752
|
+
h.setAttribute('aria-valuenow', String(Math.round(v)));
|
|
753
|
+
}
|
|
754
|
+
count(el) {
|
|
755
|
+
el.dataset.counted = '';
|
|
756
|
+
const target = el.dataset.count || el.textContent || '0';
|
|
757
|
+
el.setAttribute('aria-label', target);
|
|
758
|
+
if (this.reduced) {
|
|
759
|
+
el.textContent = target;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const dur = Number(el.dataset.duration) || 1600;
|
|
763
|
+
const t0 = performance.now();
|
|
764
|
+
const tick = (now) => {
|
|
765
|
+
const k = clamp((now - t0) / dur);
|
|
766
|
+
el.textContent = formatCount(target, 1 - (1 - k) ** 3);
|
|
767
|
+
if (k < 1)
|
|
768
|
+
requestAnimationFrame(tick);
|
|
769
|
+
};
|
|
770
|
+
requestAnimationFrame(tick);
|
|
771
|
+
}
|
|
772
|
+
mount() {
|
|
773
|
+
this.toggleAttribute('data-static', this.reduced);
|
|
774
|
+
if (this.template === 'compare')
|
|
775
|
+
this.mountCompare();
|
|
776
|
+
const kick = () => {
|
|
777
|
+
if (!this.frame)
|
|
778
|
+
this.frame = requestAnimationFrame(() => this.update());
|
|
779
|
+
};
|
|
780
|
+
this.listen(window, 'scroll', kick, { passive: true });
|
|
781
|
+
this.listen(window, 'resize', kick);
|
|
782
|
+
this.onCleanup(() => cancelAnimationFrame(this.frame));
|
|
783
|
+
this.update();
|
|
784
|
+
}
|
|
785
|
+
mountCompare() {
|
|
786
|
+
const box = this.querySelector('[data-sticky]') || this;
|
|
787
|
+
let h = this.querySelector('[data-handle]');
|
|
788
|
+
if (!h) {
|
|
789
|
+
h = document.createElement('div');
|
|
790
|
+
h.setAttribute('data-handle', '');
|
|
791
|
+
box.appendChild(h);
|
|
792
|
+
this.onCleanup(() => h.remove());
|
|
793
|
+
}
|
|
794
|
+
h.tabIndex = 0;
|
|
795
|
+
h.setAttribute('role', 'slider');
|
|
796
|
+
h.setAttribute('aria-label', this.str('label', 'Before / after'));
|
|
797
|
+
h.setAttribute('aria-valuemin', '0');
|
|
798
|
+
h.setAttribute('aria-valuemax', '100');
|
|
799
|
+
const at = (e) => {
|
|
800
|
+
const r = box.getBoundingClientRect();
|
|
801
|
+
this.dataset.dragged = '';
|
|
802
|
+
this.setSplit(((e.clientX - r.left) / (r.width || 1)) * 100);
|
|
803
|
+
};
|
|
804
|
+
let drag = false;
|
|
805
|
+
this.listen(h, 'pointerdown', (e) => {
|
|
806
|
+
drag = true;
|
|
807
|
+
h.setPointerCapture?.(e.pointerId);
|
|
808
|
+
});
|
|
809
|
+
this.listen(h, 'pointermove', (e) => drag && at(e));
|
|
810
|
+
this.listen(h, 'pointerup', () => (drag = false));
|
|
811
|
+
this.listen(h, 'keydown', (e) => {
|
|
812
|
+
const cur = parseFloat(h.getAttribute('aria-valuenow') || '50');
|
|
813
|
+
const step = e.shiftKey ? 10 : 2;
|
|
814
|
+
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;
|
|
815
|
+
if (next == null)
|
|
816
|
+
return;
|
|
817
|
+
e.preventDefault();
|
|
818
|
+
this.dataset.dragged = '';
|
|
819
|
+
this.setSplit(next);
|
|
820
|
+
});
|
|
821
|
+
this.setSplit(50);
|
|
822
|
+
}
|
|
823
|
+
unmount() {
|
|
824
|
+
this.step = -1;
|
|
825
|
+
delete this.dataset.dragged;
|
|
826
|
+
}
|
|
827
|
+
}, { id: 'usa-story', text: CSS });
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Deterministic smooth pseudo-noise in [-1, 1] (sum of sines — cheap, no tables). */
|
|
831
|
+
function noise2(x, y, t = 0) {
|
|
832
|
+
return (Math.sin(x * 1.7 + t) * Math.cos(y * 1.3 - t * 0.7) + Math.sin((x + y) * 0.9 + t * 0.5) * 0.5 + Math.cos(x * 0.6 - y * 1.1 - t * 0.3) * 0.5) / 2;
|
|
833
|
+
}
|
|
834
|
+
/** Hex `#rrggbb` → [r, g, b]. */
|
|
835
|
+
function hexRgb(hex) {
|
|
836
|
+
const m = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(hex.trim());
|
|
837
|
+
return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [124, 92, 255];
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Mount a generative canvas behind `el` and run `spec` on it. Returns the
|
|
841
|
+
* cleanup. Exposed for custom generative effects.
|
|
842
|
+
*/
|
|
843
|
+
function canvasBackground(el, fx, spec, o) {
|
|
844
|
+
const canvas = document.createElement('canvas');
|
|
845
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
846
|
+
canvas.setAttribute('data-usa-fx-canvas', '');
|
|
847
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:-1;border-radius:inherit';
|
|
848
|
+
const restore = [];
|
|
849
|
+
const set = (k, v) => {
|
|
850
|
+
restore.push([k, el.style[k]]);
|
|
851
|
+
el.style[k] = v;
|
|
852
|
+
};
|
|
853
|
+
if (getComputedStyle(el).position === 'static')
|
|
854
|
+
set('position', 'relative');
|
|
855
|
+
set('isolation', 'isolate');
|
|
856
|
+
el.prepend(canvas);
|
|
857
|
+
const ctx = canvas.getContext('2d');
|
|
858
|
+
let raf = 0;
|
|
859
|
+
let visible = true;
|
|
860
|
+
let quality = Math.min(1, Math.max(0.35, Number(o.quality) || 1));
|
|
861
|
+
let w = 0;
|
|
862
|
+
let h = 0;
|
|
863
|
+
let state = null;
|
|
864
|
+
const t0 = performance.now();
|
|
865
|
+
let last = t0;
|
|
866
|
+
let slow = 0;
|
|
867
|
+
let fast = 0;
|
|
868
|
+
const resize = () => {
|
|
869
|
+
const r = el.getBoundingClientRect();
|
|
870
|
+
w = Math.max(1, Math.round(r.width));
|
|
871
|
+
h = Math.max(1, Math.round(r.height));
|
|
872
|
+
const dpr = Math.min(2, (typeof devicePixelRatio === 'number' ? devicePixelRatio : 1) || 1) * quality;
|
|
873
|
+
canvas.width = Math.max(1, Math.round(w * dpr));
|
|
874
|
+
canvas.height = Math.max(1, Math.round(h * dpr));
|
|
875
|
+
if (ctx)
|
|
876
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
877
|
+
state = spec.init ? spec.init(w, h, o) : {};
|
|
878
|
+
};
|
|
879
|
+
const frame = (now) => {
|
|
880
|
+
raf = 0;
|
|
881
|
+
if (!ctx)
|
|
882
|
+
return;
|
|
883
|
+
const dt = now - last;
|
|
884
|
+
last = now;
|
|
885
|
+
// adaptive quality: sustained slow frames lower the render scale, fast ones raise it
|
|
886
|
+
if (dt > 28)
|
|
887
|
+
slow++;
|
|
888
|
+
else if (dt < 18)
|
|
889
|
+
fast++;
|
|
890
|
+
if (slow > 20 && quality > 0.35) {
|
|
891
|
+
quality = Math.max(0.35, quality - 0.15);
|
|
892
|
+
slow = fast = 0;
|
|
893
|
+
resize();
|
|
894
|
+
}
|
|
895
|
+
else if (fast > 240 && quality < 1) {
|
|
896
|
+
quality = Math.min(1, quality + 0.15);
|
|
897
|
+
slow = fast = 0;
|
|
898
|
+
resize();
|
|
899
|
+
}
|
|
900
|
+
spec.draw({ ctx, w, h, t: (now - t0) / 1000, quality, state, o });
|
|
901
|
+
if (visible && !document.hidden)
|
|
902
|
+
raf = requestAnimationFrame(frame);
|
|
903
|
+
};
|
|
904
|
+
const start = () => {
|
|
905
|
+
if (!raf && ctx && !fx.reduced) {
|
|
906
|
+
last = performance.now();
|
|
907
|
+
raf = requestAnimationFrame(frame);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
resize();
|
|
911
|
+
if (ctx)
|
|
912
|
+
spec.draw({ ctx, w, h, t: 0, quality, state, o }); // first / static frame
|
|
913
|
+
const io = typeof IntersectionObserver === 'function' ? new IntersectionObserver((es) => {
|
|
914
|
+
visible = es.some((e) => e.isIntersecting);
|
|
915
|
+
if (visible)
|
|
916
|
+
start();
|
|
917
|
+
}) : null;
|
|
918
|
+
io?.observe(el);
|
|
919
|
+
const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(() => {
|
|
920
|
+
resize();
|
|
921
|
+
if (ctx && fx.reduced)
|
|
922
|
+
spec.draw({ ctx, w, h, t: 0, quality, state, o });
|
|
923
|
+
}) : null;
|
|
924
|
+
ro?.observe(el);
|
|
925
|
+
const vis = () => !document.hidden && visible && start();
|
|
926
|
+
document.addEventListener('visibilitychange', vis);
|
|
927
|
+
start();
|
|
928
|
+
return () => {
|
|
929
|
+
cancelAnimationFrame(raf);
|
|
930
|
+
raf = 0;
|
|
931
|
+
io?.disconnect();
|
|
932
|
+
ro?.disconnect();
|
|
933
|
+
document.removeEventListener('visibilitychange', vis);
|
|
934
|
+
canvas.remove();
|
|
935
|
+
for (const [k, v] of restore)
|
|
936
|
+
el.style[k] = v;
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const gen = (name, description, defaults, spec) => ({
|
|
940
|
+
name,
|
|
941
|
+
kind: 'background',
|
|
942
|
+
description,
|
|
943
|
+
reduced: 'run',
|
|
944
|
+
defaults: { colors: PALETTE, background: '#0b0d12', speed: 1, quality: 1, ...defaults },
|
|
945
|
+
run: (el, o, ctx) => canvasBackground(el, ctx, spec, o),
|
|
946
|
+
});
|
|
947
|
+
/** Low-resolution scalar field → ImageData, drawn scaled up (used by voronoi / metaballs / contours). */
|
|
948
|
+
function field(f, cell, px) {
|
|
949
|
+
const step = Math.max(2, Math.round(cell / f.quality));
|
|
950
|
+
const cw = Math.max(1, Math.ceil(f.w / step));
|
|
951
|
+
const ch = Math.max(1, Math.ceil(f.h / step));
|
|
952
|
+
const st = f.state;
|
|
953
|
+
if (!st.buf || st.buf.width !== cw || st.buf.height !== ch) {
|
|
954
|
+
st.buf = document.createElement('canvas');
|
|
955
|
+
st.buf.width = cw;
|
|
956
|
+
st.buf.height = ch;
|
|
957
|
+
st.bctx = st.buf.getContext('2d');
|
|
958
|
+
st.img = st.bctx?.createImageData(cw, ch);
|
|
959
|
+
}
|
|
960
|
+
if (!st.bctx || !st.img)
|
|
961
|
+
return;
|
|
962
|
+
const d = st.img.data;
|
|
963
|
+
for (let y = 0; y < ch; y++)
|
|
964
|
+
for (let x = 0; x < cw; x++)
|
|
965
|
+
px(x * step, y * step, d, (y * cw + x) * 4);
|
|
966
|
+
st.bctx.putImageData(st.img, 0, 0);
|
|
967
|
+
f.ctx.imageSmoothingEnabled = true;
|
|
968
|
+
f.ctx.drawImage(st.buf, 0, 0, f.w, f.h);
|
|
969
|
+
}
|
|
970
|
+
const GENERATIVE_FX = [
|
|
971
|
+
gen('flow-field', 'Particles drift along a slowly changing flow field, leaving silky trails.', { count: 600, fade: 0.06 }, {
|
|
972
|
+
init: (w, h, o) => ({ p: Array.from({ length: o.count }, () => [Math.random() * w, Math.random() * h]) }),
|
|
973
|
+
draw: ({ ctx, w, h, t, state, o, quality }) => {
|
|
974
|
+
ctx.fillStyle = o.background;
|
|
975
|
+
ctx.globalAlpha = t === 0 ? 1 : o.fade;
|
|
976
|
+
ctx.fillRect(0, 0, w, h);
|
|
977
|
+
ctx.globalAlpha = 0.8;
|
|
978
|
+
const n = Math.round(state.p.length * quality);
|
|
979
|
+
const steps = t === 0 ? 40 : 1;
|
|
980
|
+
for (let i = 0; i < n; i++) {
|
|
981
|
+
const q = state.p[i];
|
|
982
|
+
ctx.fillStyle = o.colors[i % o.colors.length];
|
|
983
|
+
for (let s = 0; s < steps; s++) {
|
|
984
|
+
const a = noise2(q[0] * 0.004, q[1] * 0.004, t * 0.15 * o.speed) * Math.PI * 2;
|
|
985
|
+
q[0] += Math.cos(a) * 1.2 * o.speed;
|
|
986
|
+
q[1] += Math.sin(a) * 1.2 * o.speed;
|
|
987
|
+
if (q[0] < 0 || q[0] > w || q[1] < 0 || q[1] > h)
|
|
988
|
+
(q[0] = Math.random() * w), (q[1] = Math.random() * h);
|
|
989
|
+
ctx.fillRect(q[0], q[1], 1.4, 1.4);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
ctx.globalAlpha = 1;
|
|
993
|
+
},
|
|
994
|
+
}),
|
|
995
|
+
gen('voronoi', 'Drifting Voronoi cells in the palette, with darker borders.', { seeds: 14, cell: 6 }, {
|
|
996
|
+
init: (w, h, o) => ({ s: Array.from({ length: o.seeds }, (_, i) => ({ x: Math.random() * w, y: Math.random() * h, vx: Math.random() - 0.5, vy: Math.random() - 0.5, c: hexRgb(o.colors[i % o.colors.length]) })) }),
|
|
997
|
+
draw: (f) => {
|
|
998
|
+
const { w, h, state, o } = f;
|
|
999
|
+
for (const s of state.s) {
|
|
1000
|
+
s.x += s.vx * o.speed;
|
|
1001
|
+
s.y += s.vy * o.speed;
|
|
1002
|
+
if (s.x < 0 || s.x > w)
|
|
1003
|
+
s.vx *= -1;
|
|
1004
|
+
if (s.y < 0 || s.y > h)
|
|
1005
|
+
s.vy *= -1;
|
|
1006
|
+
}
|
|
1007
|
+
field(f, o.cell, (x, y, d, i) => {
|
|
1008
|
+
let b = Infinity;
|
|
1009
|
+
let b2 = Infinity;
|
|
1010
|
+
let c = state.s[0].c;
|
|
1011
|
+
for (const s of state.s) {
|
|
1012
|
+
const dd = (s.x - x) ** 2 + (s.y - y) ** 2;
|
|
1013
|
+
if (dd < b)
|
|
1014
|
+
(b2 = b), (b = dd), (c = s.c);
|
|
1015
|
+
else if (dd < b2)
|
|
1016
|
+
b2 = dd;
|
|
1017
|
+
}
|
|
1018
|
+
const edge = Math.sqrt(b2) - Math.sqrt(b) < 6 ? 0.55 : 1;
|
|
1019
|
+
d[i] = c[0] * edge;
|
|
1020
|
+
d[i + 1] = c[1] * edge;
|
|
1021
|
+
d[i + 2] = c[2] * edge;
|
|
1022
|
+
d[i + 3] = 255;
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
}),
|
|
1026
|
+
gen('mesh-gradient', 'Soft blobs of colour orbit and blend into a living mesh gradient.', { blobs: 4, blur: 0.65 }, {
|
|
1027
|
+
draw: ({ ctx, w, h, t, o }) => {
|
|
1028
|
+
ctx.fillStyle = o.background;
|
|
1029
|
+
ctx.fillRect(0, 0, w, h);
|
|
1030
|
+
const r = Math.max(w, h) * o.blur;
|
|
1031
|
+
for (let i = 0; i < o.blobs; i++) {
|
|
1032
|
+
const a = t * 0.25 * o.speed + (i * Math.PI * 2) / o.blobs;
|
|
1033
|
+
const x = w / 2 + Math.cos(a * (1 + i * 0.13)) * w * 0.32;
|
|
1034
|
+
const y = h / 2 + Math.sin(a * (0.8 + i * 0.11)) * h * 0.32;
|
|
1035
|
+
const [cr, cg, cb] = hexRgb(o.colors[i % o.colors.length]);
|
|
1036
|
+
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
|
1037
|
+
g.addColorStop(0, `rgba(${cr},${cg},${cb},0.7)`);
|
|
1038
|
+
g.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
|
|
1039
|
+
ctx.fillStyle = g;
|
|
1040
|
+
ctx.fillRect(0, 0, w, h);
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
}),
|
|
1044
|
+
gen('starfield', 'Stars stream toward the viewer (warp speed with `speed: 4`).', { stars: 400, color: '#ffffff' }, {
|
|
1045
|
+
init: (_w, _h, o) => ({ s: Array.from({ length: o.stars }, () => [Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random()]) }),
|
|
1046
|
+
draw: ({ ctx, w, h, t, state, o, quality }) => {
|
|
1047
|
+
ctx.fillStyle = o.background;
|
|
1048
|
+
ctx.fillRect(0, 0, w, h);
|
|
1049
|
+
ctx.fillStyle = o.color;
|
|
1050
|
+
const n = Math.round(state.s.length * quality);
|
|
1051
|
+
for (let i = 0; i < n; i++) {
|
|
1052
|
+
const s = state.s[i];
|
|
1053
|
+
if (t > 0)
|
|
1054
|
+
s[2] -= 0.004 * o.speed;
|
|
1055
|
+
if (s[2] <= 0.01)
|
|
1056
|
+
(s[0] = Math.random() * 2 - 1), (s[1] = Math.random() * 2 - 1), (s[2] = 1);
|
|
1057
|
+
const x = w / 2 + (s[0] / s[2]) * w * 0.5;
|
|
1058
|
+
const y = h / 2 + (s[1] / s[2]) * h * 0.5;
|
|
1059
|
+
if (x < 0 || x > w || y < 0 || y > h)
|
|
1060
|
+
continue;
|
|
1061
|
+
const size = (1 - s[2]) * 2.6;
|
|
1062
|
+
ctx.globalAlpha = Math.min(1, (1 - s[2]) * 1.6);
|
|
1063
|
+
ctx.fillRect(x, y, size, size);
|
|
1064
|
+
}
|
|
1065
|
+
ctx.globalAlpha = 1;
|
|
1066
|
+
},
|
|
1067
|
+
}),
|
|
1068
|
+
gen('metaballs', 'Gooey blobs that merge and split (low-resolution field, scaled up).', { balls: 6, cell: 5, threshold: 1 }, {
|
|
1069
|
+
init: (w, h, o) => ({ b: Array.from({ length: o.balls }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 1.6, vy: (Math.random() - 0.5) * 1.6, r: Math.min(w, h) * (0.08 + Math.random() * 0.08) })) }),
|
|
1070
|
+
draw: (f) => {
|
|
1071
|
+
const { w, h, state, o } = f;
|
|
1072
|
+
for (const b of state.b) {
|
|
1073
|
+
b.x += b.vx * o.speed;
|
|
1074
|
+
b.y += b.vy * o.speed;
|
|
1075
|
+
if (b.x < 0 || b.x > w)
|
|
1076
|
+
b.vx *= -1;
|
|
1077
|
+
if (b.y < 0 || b.y > h)
|
|
1078
|
+
b.vy *= -1;
|
|
1079
|
+
}
|
|
1080
|
+
const [ar, ag, ab] = hexRgb(o.colors[0]);
|
|
1081
|
+
const [br, bg, bb] = hexRgb(o.colors[1] || o.colors[0]);
|
|
1082
|
+
const [kr, kg, kb] = hexRgb(o.background);
|
|
1083
|
+
field(f, o.cell, (x, y, d, i) => {
|
|
1084
|
+
let v = 0;
|
|
1085
|
+
for (const b of state.b)
|
|
1086
|
+
v += (b.r * b.r) / ((b.x - x) ** 2 + (b.y - y) ** 2 + 1);
|
|
1087
|
+
const inside = v >= o.threshold;
|
|
1088
|
+
const k = Math.min(1, (v - o.threshold) * 0.8);
|
|
1089
|
+
d[i] = inside ? ar + (br - ar) * k : kr;
|
|
1090
|
+
d[i + 1] = inside ? ag + (bg - ag) * k : kg;
|
|
1091
|
+
d[i + 2] = inside ? ab + (bb - ab) * k : kb;
|
|
1092
|
+
d[i + 3] = 255;
|
|
1093
|
+
});
|
|
1094
|
+
},
|
|
1095
|
+
}),
|
|
1096
|
+
gen('contours', 'Topographic contour lines over a slowly shifting height field.', { levels: 9, cell: 4, color: '#7c5cff' }, {
|
|
1097
|
+
draw: (f) => {
|
|
1098
|
+
const { t, o } = f;
|
|
1099
|
+
const [lr, lg, lb] = hexRgb(o.color);
|
|
1100
|
+
const [kr, kg, kb] = hexRgb(o.background);
|
|
1101
|
+
const z = (x, y) => (noise2(x * 0.006, y * 0.006, t * 0.2 * o.speed) + 1) / 2;
|
|
1102
|
+
const step = Math.max(2, Math.round(o.cell / f.quality));
|
|
1103
|
+
field(f, o.cell, (x, y, d, i) => {
|
|
1104
|
+
const a = Math.floor(z(x, y) * o.levels);
|
|
1105
|
+
const line = a !== Math.floor(z(x + step, y) * o.levels) || a !== Math.floor(z(x, y + step) * o.levels);
|
|
1106
|
+
const shade = 0.12 + (a / o.levels) * 0.18;
|
|
1107
|
+
d[i] = line ? lr : kr + (lr - kr) * shade;
|
|
1108
|
+
d[i + 1] = line ? lg : kg + (lg - kg) * shade;
|
|
1109
|
+
d[i + 2] = line ? lb : kb + (lb - kb) * shade;
|
|
1110
|
+
d[i + 3] = 255;
|
|
1111
|
+
});
|
|
1112
|
+
},
|
|
1113
|
+
}),
|
|
1114
|
+
];
|
|
1115
|
+
|
|
620
1116
|
/**
|
|
621
1117
|
* use-scroll-animate/components/effects — the 5.x effect packs, all
|
|
622
1118
|
* registered through `registerEffect()` (5.0) and playable with
|
|
@@ -634,6 +1130,7 @@ const EFFECT_PACKS = {
|
|
|
634
1130
|
'cards-click': [...CARD_FX, ...CLICK_FX],
|
|
635
1131
|
physics: PHYSICS_FX,
|
|
636
1132
|
page: PAGE_FX,
|
|
1133
|
+
generative: GENERATIVE_FX,
|
|
637
1134
|
};
|
|
638
1135
|
/** 5.1: card & click effects 2.0. */
|
|
639
1136
|
function registerCardClickEffects() {
|
|
@@ -647,6 +1144,14 @@ function registerPhysicsEffects() {
|
|
|
647
1144
|
function registerPageEffects() {
|
|
648
1145
|
registerEffects(EFFECT_PACKS.page);
|
|
649
1146
|
}
|
|
1147
|
+
/** 5.5: generative Canvas 2D backgrounds. */
|
|
1148
|
+
function registerGenerativeEffects() {
|
|
1149
|
+
registerEffects(EFFECT_PACKS.generative);
|
|
1150
|
+
}
|
|
1151
|
+
/** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
|
|
1152
|
+
function defineEffectElements() {
|
|
1153
|
+
defineStory();
|
|
1154
|
+
}
|
|
650
1155
|
/** Register the built-ins and every pack (idempotent). */
|
|
651
1156
|
function registerAllEffects() {
|
|
652
1157
|
registerBuiltinEffects();
|
|
@@ -654,5 +1159,5 @@ function registerAllEffects() {
|
|
|
654
1159
|
registerEffects(defs);
|
|
655
1160
|
}
|
|
656
1161
|
|
|
657
|
-
export { CARD_FX, CLICK_FX, EFFECT_PACKS, PAGE_FX, PHYSICS_FX, bounceKeyframes, fxLayer, registerAllEffects, registerCardClickEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes };
|
|
1162
|
+
export { CARD_FX, CLICK_FX, EFFECT_PACKS, GENERATIVE_FX, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, bounceKeyframes, canvasBackground, defineEffectElements, defineStory, formatCount, fxLayer, hexRgb, noise2, registerAllEffects, registerCardClickEffects, registerGenerativeEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
|
|
658
1163
|
//# sourceMappingURL=effects.js.map
|