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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.5.0] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Generative backgrounds pack** (`use-scroll-animate/components/effects`, `registerGenerativeEffects()`, `GENERATIVE_FX`, kind `background`): `flow-field`, `voronoi`, `mesh-gradient`, `starfield`, `metaballs`, `contours` — Canvas 2D, options `colors`, `background`, `speed`, `quality` plus per-effect knobs (`count`, `seeds`, `blobs`, `stars`, `balls`, `levels`, `cell`…).
|
|
14
|
+
- `canvasBackground(el, ctx, { init, draw }, options)` — the shared runner, exported for custom generative effects: an `aria-hidden`, pointer-transparent canvas behind the content (`isolation: isolate`), renders only while visible (IntersectionObserver) and the tab is shown, resizes with the element, and adapts quality (render scale 0.35–1 drops on sustained slow frames, recovers on fast ones). Helpers `noise2()`, `hexRgb()`.
|
|
15
|
+
- Showcase: **Generative backgrounds** card (switch between all six).
|
|
16
|
+
|
|
17
|
+
### Accessibility
|
|
18
|
+
- Under reduced motion each background draws one static frame and never loops; canvases are `aria-hidden` and never take pointer events.
|
|
19
|
+
|
|
20
|
+
## [5.4.0] - 2026-10-08
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`<usa-story template="…">`** (`defineStory()` from `use-scroll-animate/components/effects`; `defineEffectElements()` defines every element of the entry) — six scroll-storytelling templates:
|
|
24
|
+
- `pin` — a sticky `[data-stage]` while `[data-step]` sections scroll past; the active step gets `data-active`, the stage `data-active-step="<i>"`.
|
|
25
|
+
- `gallery` — a horizontal `[data-track]` slides sideways as you scroll down.
|
|
26
|
+
- `zoom` — zoom-through: the stage scales up to `zoom="6"` and fades.
|
|
27
|
+
- `compare` — before / after wipe driven by scroll, with a draggable, keyboard-accessible handle (`role="slider"`, ←/→, Shift for ×5, Home/End).
|
|
28
|
+
- `counter` — `[data-count="12,480"]` numbers count up on entering view (separators, decimals, prefix / suffix kept).
|
|
29
|
+
- `highlight` — the paragraph (or `[data-step]`) crossing the viewport center is highlighted.
|
|
30
|
+
- 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`.
|
|
31
|
+
- 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.
|
|
32
|
+
|
|
33
|
+
### Accessibility
|
|
34
|
+
- 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`.
|
|
35
|
+
|
|
10
36
|
## [5.3.0] - 2026-10-08
|
|
11
37
|
|
|
12
38
|
### 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');
|
|
@@ -619,6 +619,502 @@ const PAGE_FX = [
|
|
|
619
619
|
},
|
|
620
620
|
];
|
|
621
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
|
+
|
|
832
|
+
/** Deterministic smooth pseudo-noise in [-1, 1] (sum of sines — cheap, no tables). */
|
|
833
|
+
function noise2(x, y, t = 0) {
|
|
834
|
+
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;
|
|
835
|
+
}
|
|
836
|
+
/** Hex `#rrggbb` → [r, g, b]. */
|
|
837
|
+
function hexRgb(hex) {
|
|
838
|
+
const m = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(hex.trim());
|
|
839
|
+
return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [124, 92, 255];
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Mount a generative canvas behind `el` and run `spec` on it. Returns the
|
|
843
|
+
* cleanup. Exposed for custom generative effects.
|
|
844
|
+
*/
|
|
845
|
+
function canvasBackground(el, fx, spec, o) {
|
|
846
|
+
const canvas = document.createElement('canvas');
|
|
847
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
848
|
+
canvas.setAttribute('data-usa-fx-canvas', '');
|
|
849
|
+
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:-1;border-radius:inherit';
|
|
850
|
+
const restore = [];
|
|
851
|
+
const set = (k, v) => {
|
|
852
|
+
restore.push([k, el.style[k]]);
|
|
853
|
+
el.style[k] = v;
|
|
854
|
+
};
|
|
855
|
+
if (getComputedStyle(el).position === 'static')
|
|
856
|
+
set('position', 'relative');
|
|
857
|
+
set('isolation', 'isolate');
|
|
858
|
+
el.prepend(canvas);
|
|
859
|
+
const ctx = canvas.getContext('2d');
|
|
860
|
+
let raf = 0;
|
|
861
|
+
let visible = true;
|
|
862
|
+
let quality = Math.min(1, Math.max(0.35, Number(o.quality) || 1));
|
|
863
|
+
let w = 0;
|
|
864
|
+
let h = 0;
|
|
865
|
+
let state = null;
|
|
866
|
+
const t0 = performance.now();
|
|
867
|
+
let last = t0;
|
|
868
|
+
let slow = 0;
|
|
869
|
+
let fast = 0;
|
|
870
|
+
const resize = () => {
|
|
871
|
+
const r = el.getBoundingClientRect();
|
|
872
|
+
w = Math.max(1, Math.round(r.width));
|
|
873
|
+
h = Math.max(1, Math.round(r.height));
|
|
874
|
+
const dpr = Math.min(2, (typeof devicePixelRatio === 'number' ? devicePixelRatio : 1) || 1) * quality;
|
|
875
|
+
canvas.width = Math.max(1, Math.round(w * dpr));
|
|
876
|
+
canvas.height = Math.max(1, Math.round(h * dpr));
|
|
877
|
+
if (ctx)
|
|
878
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
879
|
+
state = spec.init ? spec.init(w, h, o) : {};
|
|
880
|
+
};
|
|
881
|
+
const frame = (now) => {
|
|
882
|
+
raf = 0;
|
|
883
|
+
if (!ctx)
|
|
884
|
+
return;
|
|
885
|
+
const dt = now - last;
|
|
886
|
+
last = now;
|
|
887
|
+
// adaptive quality: sustained slow frames lower the render scale, fast ones raise it
|
|
888
|
+
if (dt > 28)
|
|
889
|
+
slow++;
|
|
890
|
+
else if (dt < 18)
|
|
891
|
+
fast++;
|
|
892
|
+
if (slow > 20 && quality > 0.35) {
|
|
893
|
+
quality = Math.max(0.35, quality - 0.15);
|
|
894
|
+
slow = fast = 0;
|
|
895
|
+
resize();
|
|
896
|
+
}
|
|
897
|
+
else if (fast > 240 && quality < 1) {
|
|
898
|
+
quality = Math.min(1, quality + 0.15);
|
|
899
|
+
slow = fast = 0;
|
|
900
|
+
resize();
|
|
901
|
+
}
|
|
902
|
+
spec.draw({ ctx, w, h, t: (now - t0) / 1000, quality, state, o });
|
|
903
|
+
if (visible && !document.hidden)
|
|
904
|
+
raf = requestAnimationFrame(frame);
|
|
905
|
+
};
|
|
906
|
+
const start = () => {
|
|
907
|
+
if (!raf && ctx && !fx.reduced) {
|
|
908
|
+
last = performance.now();
|
|
909
|
+
raf = requestAnimationFrame(frame);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
resize();
|
|
913
|
+
if (ctx)
|
|
914
|
+
spec.draw({ ctx, w, h, t: 0, quality, state, o }); // first / static frame
|
|
915
|
+
const io = typeof IntersectionObserver === 'function' ? new IntersectionObserver((es) => {
|
|
916
|
+
visible = es.some((e) => e.isIntersecting);
|
|
917
|
+
if (visible)
|
|
918
|
+
start();
|
|
919
|
+
}) : null;
|
|
920
|
+
io?.observe(el);
|
|
921
|
+
const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(() => {
|
|
922
|
+
resize();
|
|
923
|
+
if (ctx && fx.reduced)
|
|
924
|
+
spec.draw({ ctx, w, h, t: 0, quality, state, o });
|
|
925
|
+
}) : null;
|
|
926
|
+
ro?.observe(el);
|
|
927
|
+
const vis = () => !document.hidden && visible && start();
|
|
928
|
+
document.addEventListener('visibilitychange', vis);
|
|
929
|
+
start();
|
|
930
|
+
return () => {
|
|
931
|
+
cancelAnimationFrame(raf);
|
|
932
|
+
raf = 0;
|
|
933
|
+
io?.disconnect();
|
|
934
|
+
ro?.disconnect();
|
|
935
|
+
document.removeEventListener('visibilitychange', vis);
|
|
936
|
+
canvas.remove();
|
|
937
|
+
for (const [k, v] of restore)
|
|
938
|
+
el.style[k] = v;
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
const gen = (name, description, defaults, spec) => ({
|
|
942
|
+
name,
|
|
943
|
+
kind: 'background',
|
|
944
|
+
description,
|
|
945
|
+
reduced: 'run',
|
|
946
|
+
defaults: { colors: PALETTE, background: '#0b0d12', speed: 1, quality: 1, ...defaults },
|
|
947
|
+
run: (el, o, ctx) => canvasBackground(el, ctx, spec, o),
|
|
948
|
+
});
|
|
949
|
+
/** Low-resolution scalar field → ImageData, drawn scaled up (used by voronoi / metaballs / contours). */
|
|
950
|
+
function field(f, cell, px) {
|
|
951
|
+
const step = Math.max(2, Math.round(cell / f.quality));
|
|
952
|
+
const cw = Math.max(1, Math.ceil(f.w / step));
|
|
953
|
+
const ch = Math.max(1, Math.ceil(f.h / step));
|
|
954
|
+
const st = f.state;
|
|
955
|
+
if (!st.buf || st.buf.width !== cw || st.buf.height !== ch) {
|
|
956
|
+
st.buf = document.createElement('canvas');
|
|
957
|
+
st.buf.width = cw;
|
|
958
|
+
st.buf.height = ch;
|
|
959
|
+
st.bctx = st.buf.getContext('2d');
|
|
960
|
+
st.img = st.bctx?.createImageData(cw, ch);
|
|
961
|
+
}
|
|
962
|
+
if (!st.bctx || !st.img)
|
|
963
|
+
return;
|
|
964
|
+
const d = st.img.data;
|
|
965
|
+
for (let y = 0; y < ch; y++)
|
|
966
|
+
for (let x = 0; x < cw; x++)
|
|
967
|
+
px(x * step, y * step, d, (y * cw + x) * 4);
|
|
968
|
+
st.bctx.putImageData(st.img, 0, 0);
|
|
969
|
+
f.ctx.imageSmoothingEnabled = true;
|
|
970
|
+
f.ctx.drawImage(st.buf, 0, 0, f.w, f.h);
|
|
971
|
+
}
|
|
972
|
+
const GENERATIVE_FX = [
|
|
973
|
+
gen('flow-field', 'Particles drift along a slowly changing flow field, leaving silky trails.', { count: 600, fade: 0.06 }, {
|
|
974
|
+
init: (w, h, o) => ({ p: Array.from({ length: o.count }, () => [Math.random() * w, Math.random() * h]) }),
|
|
975
|
+
draw: ({ ctx, w, h, t, state, o, quality }) => {
|
|
976
|
+
ctx.fillStyle = o.background;
|
|
977
|
+
ctx.globalAlpha = t === 0 ? 1 : o.fade;
|
|
978
|
+
ctx.fillRect(0, 0, w, h);
|
|
979
|
+
ctx.globalAlpha = 0.8;
|
|
980
|
+
const n = Math.round(state.p.length * quality);
|
|
981
|
+
const steps = t === 0 ? 40 : 1;
|
|
982
|
+
for (let i = 0; i < n; i++) {
|
|
983
|
+
const q = state.p[i];
|
|
984
|
+
ctx.fillStyle = o.colors[i % o.colors.length];
|
|
985
|
+
for (let s = 0; s < steps; s++) {
|
|
986
|
+
const a = noise2(q[0] * 0.004, q[1] * 0.004, t * 0.15 * o.speed) * Math.PI * 2;
|
|
987
|
+
q[0] += Math.cos(a) * 1.2 * o.speed;
|
|
988
|
+
q[1] += Math.sin(a) * 1.2 * o.speed;
|
|
989
|
+
if (q[0] < 0 || q[0] > w || q[1] < 0 || q[1] > h)
|
|
990
|
+
(q[0] = Math.random() * w), (q[1] = Math.random() * h);
|
|
991
|
+
ctx.fillRect(q[0], q[1], 1.4, 1.4);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
ctx.globalAlpha = 1;
|
|
995
|
+
},
|
|
996
|
+
}),
|
|
997
|
+
gen('voronoi', 'Drifting Voronoi cells in the palette, with darker borders.', { seeds: 14, cell: 6 }, {
|
|
998
|
+
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]) })) }),
|
|
999
|
+
draw: (f) => {
|
|
1000
|
+
const { w, h, state, o } = f;
|
|
1001
|
+
for (const s of state.s) {
|
|
1002
|
+
s.x += s.vx * o.speed;
|
|
1003
|
+
s.y += s.vy * o.speed;
|
|
1004
|
+
if (s.x < 0 || s.x > w)
|
|
1005
|
+
s.vx *= -1;
|
|
1006
|
+
if (s.y < 0 || s.y > h)
|
|
1007
|
+
s.vy *= -1;
|
|
1008
|
+
}
|
|
1009
|
+
field(f, o.cell, (x, y, d, i) => {
|
|
1010
|
+
let b = Infinity;
|
|
1011
|
+
let b2 = Infinity;
|
|
1012
|
+
let c = state.s[0].c;
|
|
1013
|
+
for (const s of state.s) {
|
|
1014
|
+
const dd = (s.x - x) ** 2 + (s.y - y) ** 2;
|
|
1015
|
+
if (dd < b)
|
|
1016
|
+
(b2 = b), (b = dd), (c = s.c);
|
|
1017
|
+
else if (dd < b2)
|
|
1018
|
+
b2 = dd;
|
|
1019
|
+
}
|
|
1020
|
+
const edge = Math.sqrt(b2) - Math.sqrt(b) < 6 ? 0.55 : 1;
|
|
1021
|
+
d[i] = c[0] * edge;
|
|
1022
|
+
d[i + 1] = c[1] * edge;
|
|
1023
|
+
d[i + 2] = c[2] * edge;
|
|
1024
|
+
d[i + 3] = 255;
|
|
1025
|
+
});
|
|
1026
|
+
},
|
|
1027
|
+
}),
|
|
1028
|
+
gen('mesh-gradient', 'Soft blobs of colour orbit and blend into a living mesh gradient.', { blobs: 4, blur: 0.65 }, {
|
|
1029
|
+
draw: ({ ctx, w, h, t, o }) => {
|
|
1030
|
+
ctx.fillStyle = o.background;
|
|
1031
|
+
ctx.fillRect(0, 0, w, h);
|
|
1032
|
+
const r = Math.max(w, h) * o.blur;
|
|
1033
|
+
for (let i = 0; i < o.blobs; i++) {
|
|
1034
|
+
const a = t * 0.25 * o.speed + (i * Math.PI * 2) / o.blobs;
|
|
1035
|
+
const x = w / 2 + Math.cos(a * (1 + i * 0.13)) * w * 0.32;
|
|
1036
|
+
const y = h / 2 + Math.sin(a * (0.8 + i * 0.11)) * h * 0.32;
|
|
1037
|
+
const [cr, cg, cb] = hexRgb(o.colors[i % o.colors.length]);
|
|
1038
|
+
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
|
1039
|
+
g.addColorStop(0, `rgba(${cr},${cg},${cb},0.7)`);
|
|
1040
|
+
g.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
|
|
1041
|
+
ctx.fillStyle = g;
|
|
1042
|
+
ctx.fillRect(0, 0, w, h);
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
}),
|
|
1046
|
+
gen('starfield', 'Stars stream toward the viewer (warp speed with `speed: 4`).', { stars: 400, color: '#ffffff' }, {
|
|
1047
|
+
init: (_w, _h, o) => ({ s: Array.from({ length: o.stars }, () => [Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random()]) }),
|
|
1048
|
+
draw: ({ ctx, w, h, t, state, o, quality }) => {
|
|
1049
|
+
ctx.fillStyle = o.background;
|
|
1050
|
+
ctx.fillRect(0, 0, w, h);
|
|
1051
|
+
ctx.fillStyle = o.color;
|
|
1052
|
+
const n = Math.round(state.s.length * quality);
|
|
1053
|
+
for (let i = 0; i < n; i++) {
|
|
1054
|
+
const s = state.s[i];
|
|
1055
|
+
if (t > 0)
|
|
1056
|
+
s[2] -= 0.004 * o.speed;
|
|
1057
|
+
if (s[2] <= 0.01)
|
|
1058
|
+
(s[0] = Math.random() * 2 - 1), (s[1] = Math.random() * 2 - 1), (s[2] = 1);
|
|
1059
|
+
const x = w / 2 + (s[0] / s[2]) * w * 0.5;
|
|
1060
|
+
const y = h / 2 + (s[1] / s[2]) * h * 0.5;
|
|
1061
|
+
if (x < 0 || x > w || y < 0 || y > h)
|
|
1062
|
+
continue;
|
|
1063
|
+
const size = (1 - s[2]) * 2.6;
|
|
1064
|
+
ctx.globalAlpha = Math.min(1, (1 - s[2]) * 1.6);
|
|
1065
|
+
ctx.fillRect(x, y, size, size);
|
|
1066
|
+
}
|
|
1067
|
+
ctx.globalAlpha = 1;
|
|
1068
|
+
},
|
|
1069
|
+
}),
|
|
1070
|
+
gen('metaballs', 'Gooey blobs that merge and split (low-resolution field, scaled up).', { balls: 6, cell: 5, threshold: 1 }, {
|
|
1071
|
+
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) })) }),
|
|
1072
|
+
draw: (f) => {
|
|
1073
|
+
const { w, h, state, o } = f;
|
|
1074
|
+
for (const b of state.b) {
|
|
1075
|
+
b.x += b.vx * o.speed;
|
|
1076
|
+
b.y += b.vy * o.speed;
|
|
1077
|
+
if (b.x < 0 || b.x > w)
|
|
1078
|
+
b.vx *= -1;
|
|
1079
|
+
if (b.y < 0 || b.y > h)
|
|
1080
|
+
b.vy *= -1;
|
|
1081
|
+
}
|
|
1082
|
+
const [ar, ag, ab] = hexRgb(o.colors[0]);
|
|
1083
|
+
const [br, bg, bb] = hexRgb(o.colors[1] || o.colors[0]);
|
|
1084
|
+
const [kr, kg, kb] = hexRgb(o.background);
|
|
1085
|
+
field(f, o.cell, (x, y, d, i) => {
|
|
1086
|
+
let v = 0;
|
|
1087
|
+
for (const b of state.b)
|
|
1088
|
+
v += (b.r * b.r) / ((b.x - x) ** 2 + (b.y - y) ** 2 + 1);
|
|
1089
|
+
const inside = v >= o.threshold;
|
|
1090
|
+
const k = Math.min(1, (v - o.threshold) * 0.8);
|
|
1091
|
+
d[i] = inside ? ar + (br - ar) * k : kr;
|
|
1092
|
+
d[i + 1] = inside ? ag + (bg - ag) * k : kg;
|
|
1093
|
+
d[i + 2] = inside ? ab + (bb - ab) * k : kb;
|
|
1094
|
+
d[i + 3] = 255;
|
|
1095
|
+
});
|
|
1096
|
+
},
|
|
1097
|
+
}),
|
|
1098
|
+
gen('contours', 'Topographic contour lines over a slowly shifting height field.', { levels: 9, cell: 4, color: '#7c5cff' }, {
|
|
1099
|
+
draw: (f) => {
|
|
1100
|
+
const { t, o } = f;
|
|
1101
|
+
const [lr, lg, lb] = hexRgb(o.color);
|
|
1102
|
+
const [kr, kg, kb] = hexRgb(o.background);
|
|
1103
|
+
const z = (x, y) => (noise2(x * 0.006, y * 0.006, t * 0.2 * o.speed) + 1) / 2;
|
|
1104
|
+
const step = Math.max(2, Math.round(o.cell / f.quality));
|
|
1105
|
+
field(f, o.cell, (x, y, d, i) => {
|
|
1106
|
+
const a = Math.floor(z(x, y) * o.levels);
|
|
1107
|
+
const line = a !== Math.floor(z(x + step, y) * o.levels) || a !== Math.floor(z(x, y + step) * o.levels);
|
|
1108
|
+
const shade = 0.12 + (a / o.levels) * 0.18;
|
|
1109
|
+
d[i] = line ? lr : kr + (lr - kr) * shade;
|
|
1110
|
+
d[i + 1] = line ? lg : kg + (lg - kg) * shade;
|
|
1111
|
+
d[i + 2] = line ? lb : kb + (lb - kb) * shade;
|
|
1112
|
+
d[i + 3] = 255;
|
|
1113
|
+
});
|
|
1114
|
+
},
|
|
1115
|
+
}),
|
|
1116
|
+
];
|
|
1117
|
+
|
|
622
1118
|
/**
|
|
623
1119
|
* use-scroll-animate/components/effects — the 5.x effect packs, all
|
|
624
1120
|
* registered through `registerEffect()` (5.0) and playable with
|
|
@@ -636,6 +1132,7 @@ const EFFECT_PACKS = {
|
|
|
636
1132
|
'cards-click': [...CARD_FX, ...CLICK_FX],
|
|
637
1133
|
physics: PHYSICS_FX,
|
|
638
1134
|
page: PAGE_FX,
|
|
1135
|
+
generative: GENERATIVE_FX,
|
|
639
1136
|
};
|
|
640
1137
|
/** 5.1: card & click effects 2.0. */
|
|
641
1138
|
function registerCardClickEffects() {
|
|
@@ -649,6 +1146,14 @@ function registerPhysicsEffects() {
|
|
|
649
1146
|
function registerPageEffects() {
|
|
650
1147
|
components_fx.registerEffects(EFFECT_PACKS.page);
|
|
651
1148
|
}
|
|
1149
|
+
/** 5.5: generative Canvas 2D backgrounds. */
|
|
1150
|
+
function registerGenerativeEffects() {
|
|
1151
|
+
components_fx.registerEffects(EFFECT_PACKS.generative);
|
|
1152
|
+
}
|
|
1153
|
+
/** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
|
|
1154
|
+
function defineEffectElements() {
|
|
1155
|
+
defineStory();
|
|
1156
|
+
}
|
|
652
1157
|
/** Register the built-ins and every pack (idempotent). */
|
|
653
1158
|
function registerAllEffects() {
|
|
654
1159
|
components_fx.registerBuiltinEffects();
|
|
@@ -659,14 +1164,24 @@ function registerAllEffects() {
|
|
|
659
1164
|
exports.CARD_FX = CARD_FX;
|
|
660
1165
|
exports.CLICK_FX = CLICK_FX;
|
|
661
1166
|
exports.EFFECT_PACKS = EFFECT_PACKS;
|
|
1167
|
+
exports.GENERATIVE_FX = GENERATIVE_FX;
|
|
662
1168
|
exports.PAGE_FX = PAGE_FX;
|
|
663
1169
|
exports.PHYSICS_FX = PHYSICS_FX;
|
|
1170
|
+
exports.STORY_TEMPLATES = STORY_TEMPLATES;
|
|
664
1171
|
exports.bounceKeyframes = bounceKeyframes;
|
|
1172
|
+
exports.canvasBackground = canvasBackground;
|
|
1173
|
+
exports.defineEffectElements = defineEffectElements;
|
|
1174
|
+
exports.defineStory = defineStory;
|
|
1175
|
+
exports.formatCount = formatCount;
|
|
665
1176
|
exports.fxLayer = fxLayer;
|
|
1177
|
+
exports.hexRgb = hexRgb;
|
|
1178
|
+
exports.noise2 = noise2;
|
|
666
1179
|
exports.registerAllEffects = registerAllEffects;
|
|
667
1180
|
exports.registerCardClickEffects = registerCardClickEffects;
|
|
1181
|
+
exports.registerGenerativeEffects = registerGenerativeEffects;
|
|
668
1182
|
exports.registerPageEffects = registerPageEffects;
|
|
669
1183
|
exports.registerPhysicsEffects = registerPhysicsEffects;
|
|
670
1184
|
exports.solveSpring = solveSpring;
|
|
671
1185
|
exports.springKeyframes = springKeyframes;
|
|
1186
|
+
exports.storyProgress = storyProgress;
|
|
672
1187
|
//# sourceMappingURL=effects.cjs.map
|