use-scroll-animate 5.4.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.
@@ -106,6 +106,47 @@ declare const PHYSICS_FX: EffectDefinition[];
106
106
 
107
107
  declare const PAGE_FX: EffectDefinition[];
108
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
+
109
150
  /**
110
151
  * 5.4 — `<usa-story template="…">` scroll-storytelling templates.
111
152
  *
@@ -156,10 +197,12 @@ declare function registerCardClickEffects(): void;
156
197
  declare function registerPhysicsEffects(): void;
157
198
  /** 5.3: page-wide transitions and effects. */
158
199
  declare function registerPageEffects(): void;
200
+ /** 5.5: generative Canvas 2D backgrounds. */
201
+ declare function registerGenerativeEffects(): void;
159
202
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
160
203
  declare function defineEffectElements(): void;
161
204
  /** Register the built-ins and every pack (idempotent). */
162
205
  declare function registerAllEffects(): void;
163
206
 
164
- export { CARD_FX, CLICK_FX, EFFECT_PACKS, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, bounceKeyframes, defineEffectElements, defineStory, formatCount, fxLayer, registerAllEffects, registerCardClickEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
165
- export type { SpringOptions, StoryTemplate, UsaStoryElement };
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 };
@@ -827,6 +827,292 @@ function defineStory(tag = 'usa-story') {
827
827
  }, { id: 'usa-story', text: CSS });
828
828
  }
829
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
+
830
1116
  /**
831
1117
  * use-scroll-animate/components/effects — the 5.x effect packs, all
832
1118
  * registered through `registerEffect()` (5.0) and playable with
@@ -844,6 +1130,7 @@ const EFFECT_PACKS = {
844
1130
  'cards-click': [...CARD_FX, ...CLICK_FX],
845
1131
  physics: PHYSICS_FX,
846
1132
  page: PAGE_FX,
1133
+ generative: GENERATIVE_FX,
847
1134
  };
848
1135
  /** 5.1: card & click effects 2.0. */
849
1136
  function registerCardClickEffects() {
@@ -857,6 +1144,10 @@ function registerPhysicsEffects() {
857
1144
  function registerPageEffects() {
858
1145
  registerEffects(EFFECT_PACKS.page);
859
1146
  }
1147
+ /** 5.5: generative Canvas 2D backgrounds. */
1148
+ function registerGenerativeEffects() {
1149
+ registerEffects(EFFECT_PACKS.generative);
1150
+ }
860
1151
  /** Define the 5.x elements of this entry (`<usa-story>`, …) under their default tags. */
861
1152
  function defineEffectElements() {
862
1153
  defineStory();
@@ -868,5 +1159,5 @@ function registerAllEffects() {
868
1159
  registerEffects(defs);
869
1160
  }
870
1161
 
871
- export { CARD_FX, CLICK_FX, EFFECT_PACKS, PAGE_FX, PHYSICS_FX, STORY_TEMPLATES, bounceKeyframes, defineEffectElements, defineStory, formatCount, fxLayer, registerAllEffects, registerCardClickEffects, registerPageEffects, registerPhysicsEffects, solveSpring, springKeyframes, storyProgress };
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 };
872
1163
  //# sourceMappingURL=effects.js.map