@omega.js/client 0.1.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/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motion — the ONE animation engine (C3 classy v2), shared by every browser
|
|
3
|
+
* surface the same way icon-renderer is: web pages today, desktop/extension
|
|
4
|
+
* at C4. It drives the `data-omega-*` motion attributes that the core motion
|
|
5
|
+
* stylesheet (web: core/css/motion/_index.scss) styles:
|
|
6
|
+
*
|
|
7
|
+
* [data-omega-reveal="up|fade|left|right|scale"] — reveal once on scroll-in
|
|
8
|
+
* [data-omega-reveal-stagger="60"] — parent staggers child reveals (ms step)
|
|
9
|
+
* [data-omega-countup] — number counts up from 0 on first view
|
|
10
|
+
* (markup carries the FINAL text: "12,400+")
|
|
11
|
+
* [data-omega-rotate="2600"] — children cycle via data-omega-active (ms)
|
|
12
|
+
* [data-omega-marquee] — the logo set is cloned until half the
|
|
13
|
+
* track covers the container, then that
|
|
14
|
+
* half is doubled so the -50% CSS loop is
|
|
15
|
+
* seamless and NEVER runs dry; speed is a
|
|
16
|
+
* constant px/s (attr value overrides)
|
|
17
|
+
* [data-omega-scroll-watch="24"] — stamps data-omega-scrolled="true|false"
|
|
18
|
+
* when window scrolls past the threshold
|
|
19
|
+
* [data-omega-segmented] — gliding-thumb segmented control: the
|
|
20
|
+
* engine injects .omega-segmented__thumb
|
|
21
|
+
* and keeps it under the checked/.active
|
|
22
|
+
* segment (CSS transitions do the glide)
|
|
23
|
+
* [data-omega-dotfield="22"] — canvas dot grid (value = px spacing):
|
|
24
|
+
* dots breathe on a slow traveling wave
|
|
25
|
+
* and tint along ONE rainbow gradient
|
|
26
|
+
* that drifts across the field; the
|
|
27
|
+
* pointer brightens/grows nearby dots
|
|
28
|
+
* (tracked window-level so fixed
|
|
29
|
+
* overlays like the nav can't blind it).
|
|
30
|
+
* The loop starts only once first paint
|
|
31
|
+
* has settled and repaints at 30fps
|
|
32
|
+
* (#752); see whenSettled below
|
|
33
|
+
*
|
|
34
|
+
* Resilience contract (mirrors the stylesheet):
|
|
35
|
+
* - The page stamps html[data-omega-motion] via an inline head script; the
|
|
36
|
+
* stylesheet only hides reveal targets under that stamp, so no-JS pages
|
|
37
|
+
* render fully visible.
|
|
38
|
+
* - prefers-reduced-motion: reveals resolve instantly, countups render their
|
|
39
|
+
* final value, rotators hold the first word, marquees stay static,
|
|
40
|
+
* dotfields paint ONE still grid and never loop, and any `video[autoplay]`
|
|
41
|
+
* parks paused with its controls on (CSS cannot pause a video, so this
|
|
42
|
+
* lane owns it for every autoplaying band).
|
|
43
|
+
* - start() is idempotent; a MutationObserver picks up inserted content, and
|
|
44
|
+
* scan(root) is exposed for callers that render into detached roots.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const REDUCED_QUERY = '(prefers-reduced-motion: reduce)';
|
|
48
|
+
const COUNTUP_DURATION = 1200;
|
|
49
|
+
const MARQUEE_SPEED = 80; // px/s — data-omega-marquee="120" overrides per marquee
|
|
50
|
+
const DOTFIELD_FRAME = 1000 / 30; // ms: the field repaints at 30fps, not 60 (#752)
|
|
51
|
+
const DOTFIELD_FRAME_SLOP = 4; // ms: rAF jitter must never drop a beat to 20fps
|
|
52
|
+
const SETTLE_TIMEOUT = 2000; // ms: a permanently busy page still gets its field
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether the user asked for reduced motion.
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
function prefersReducedMotion() {
|
|
59
|
+
return typeof window !== 'undefined'
|
|
60
|
+
&& typeof window.matchMedia === 'function'
|
|
61
|
+
&& window.matchMedia(REDUCED_QUERY).matches;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run `fn` once first paint has settled: the first idle callback after the
|
|
66
|
+
* load event, or the load event itself where requestIdleCallback is missing
|
|
67
|
+
* (Safari). Ambient canvas work is never worth a frame of the LCP window
|
|
68
|
+
* ([#752](https://github.com/Omega-JS-Stack/omega/issues/752)).
|
|
69
|
+
* @param {Document} doc - the target's own document
|
|
70
|
+
* @param {function} fn - the work that waits for the settle signal
|
|
71
|
+
*/
|
|
72
|
+
function whenSettled(doc, fn) {
|
|
73
|
+
const idle = () => {
|
|
74
|
+
if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') {
|
|
75
|
+
window.requestIdleCallback(fn, { timeout: SETTLE_TIMEOUT });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
fn();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (doc && doc.readyState !== 'complete') {
|
|
82
|
+
window.addEventListener('load', idle, { once: true });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
idle();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse a rendered number ("12,400+", "$1.2M", "99.98%") into its parts.
|
|
91
|
+
* Grouping is inherited from the markup: "2017" stays ungrouped (years!),
|
|
92
|
+
* "12,400" keeps its commas.
|
|
93
|
+
* @param {string} text - the element's final text
|
|
94
|
+
* @returns {{ prefix: string, value: number, decimals: number, suffix: string, grouped: boolean }|null}
|
|
95
|
+
*/
|
|
96
|
+
function parseCountTarget(text) {
|
|
97
|
+
const match = String(text).trim().match(/^([^0-9-]*)(-?[\d,]*\.?\d+)(.*)$/s);
|
|
98
|
+
if (!match) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const raw = match[2].replace(/,/g, '');
|
|
103
|
+
const value = Number(raw);
|
|
104
|
+
if (!Number.isFinite(value)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const decimals = raw.includes('.') ? raw.split('.')[1].length : 0;
|
|
109
|
+
return { prefix: match[1], value, decimals, suffix: match[3], grouped: match[2].includes(',') };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format a count-up frame with the target's grouping and decimals.
|
|
114
|
+
* @param {{ prefix: string, decimals: number, suffix: string, grouped: boolean }} target
|
|
115
|
+
* @param {number} value - current frame value
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
function formatCount(target, value) {
|
|
119
|
+
const fixed = value.toFixed(target.decimals);
|
|
120
|
+
const [whole, fraction] = fixed.split('.');
|
|
121
|
+
const grouped = target.grouped ? whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',') : whole;
|
|
122
|
+
return `${target.prefix}${grouped}${fraction ? `.${fraction}` : ''}${target.suffix}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalize any CSS color (hex, rgb, named, token value) to [r, g, b] using
|
|
127
|
+
* the canvas context's own parser. Returns null for unparseable input.
|
|
128
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
129
|
+
* @param {string} value
|
|
130
|
+
* @returns {number[]|null}
|
|
131
|
+
*/
|
|
132
|
+
function parseColor(ctx, value) {
|
|
133
|
+
const raw = String(value || '').trim();
|
|
134
|
+
if (!raw) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ctx.fillStyle = '#000000';
|
|
139
|
+
ctx.fillStyle = raw;
|
|
140
|
+
const parsed = String(ctx.fillStyle);
|
|
141
|
+
|
|
142
|
+
if (parsed.startsWith('#')) {
|
|
143
|
+
return [
|
|
144
|
+
parseInt(parsed.slice(1, 3), 16),
|
|
145
|
+
parseInt(parsed.slice(3, 5), 16),
|
|
146
|
+
parseInt(parsed.slice(5, 7), 16),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const match = parsed.match(/rgba?\(([^)]+)\)/);
|
|
151
|
+
if (!match) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const parts = match[1].split(',').map((part) => parseFloat(part));
|
|
155
|
+
return [parts[0], parts[1], parts[2]];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Dotfield rainbow ramp: the classy card ring's hand-mixed PASTEL stops, in
|
|
159
|
+
// order, so the dots sweep the exact colors of the gradient ring (Ian
|
|
160
|
+
// 2026-07-31 — a generic HSL rainbow reads wrong). Mirrored from
|
|
161
|
+
// `@omega.js/web` themes/classy/css/base/_utilities.scss
|
|
162
|
+
// (--classy-gradient-stops); packages/web/test/rainbow-pin.test.js holds the
|
|
163
|
+
// two lists equal. Last stop repeats the first so the sweep wraps seamlessly.
|
|
164
|
+
const RAINBOW_STOPS = ['#f2d478', '#f2a288', '#f094c2', '#bd97f2', '#88b4f2', '#7fd9cf', '#a5e08c', '#f2d478'];
|
|
165
|
+
const RAINBOW_RGB = RAINBOW_STOPS.map((hex) => [
|
|
166
|
+
parseInt(hex.slice(1, 3), 16),
|
|
167
|
+
parseInt(hex.slice(3, 5), 16),
|
|
168
|
+
parseInt(hex.slice(5, 7), 16),
|
|
169
|
+
]);
|
|
170
|
+
const RAINBOW_SEGMENTS = RAINBOW_RGB.length - 1;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Rainbow color for a dotfield dot: the palette position advances along the
|
|
174
|
+
* grid diagonal (one full sweep ≈ 1.25 container widths) and drifts with time
|
|
175
|
+
* (a full cycle every ~20s), so every dot samples one moving gradient. The
|
|
176
|
+
* position lands inside a stop pair and blends the two per channel.
|
|
177
|
+
* @param {number} x - dot x within the field (px)
|
|
178
|
+
* @param {number} y - dot y within the field (px)
|
|
179
|
+
* @param {number} width - field width (px)
|
|
180
|
+
* @param {number} t - elapsed seconds
|
|
181
|
+
* @param {number[]} [out] - reusable output array (hot-loop friendly)
|
|
182
|
+
* @returns {number[]} [r, g, b] channels 0-255
|
|
183
|
+
*/
|
|
184
|
+
function rainbowColor(x, y, width, t, out = []) {
|
|
185
|
+
const pos = (((((x + (y * 0.35)) / (Math.max(width, 1) * 1.25)) + (t * 0.05)) % 1) + 1) % 1;
|
|
186
|
+
const scaled = pos * RAINBOW_SEGMENTS;
|
|
187
|
+
const index = Math.min(Math.floor(scaled), RAINBOW_SEGMENTS - 1);
|
|
188
|
+
const blend = scaled - index;
|
|
189
|
+
const from = RAINBOW_RGB[index];
|
|
190
|
+
const to = RAINBOW_RGB[index + 1];
|
|
191
|
+
out[0] = Math.round(from[0] + ((to[0] - from[0]) * blend));
|
|
192
|
+
out[1] = Math.round(from[1] + ((to[1] - from[1]) * blend));
|
|
193
|
+
out[2] = Math.round(from[2] + ((to[2] - from[2]) * blend));
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How many copies of the item set each track HALF needs so the -50% loop
|
|
199
|
+
* never shows a gap: the half must be at least as wide as the container.
|
|
200
|
+
* Unmeasurable geometry (jsdom, display:none) falls back to one copy.
|
|
201
|
+
* @param {number} setWidth - width of one full set of items
|
|
202
|
+
* @param {number} containerWidth - visible marquee width
|
|
203
|
+
* @returns {number}
|
|
204
|
+
*/
|
|
205
|
+
function marqueeCopies(setWidth, containerWidth) {
|
|
206
|
+
if (!(setWidth > 0) || !(containerWidth > 0)) {
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
return Math.max(1, Math.ceil(containerWidth / setWidth));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Create the motion engine. One instance per document is plenty.
|
|
214
|
+
* @returns {{ start: function, stop: function, scan: function }}
|
|
215
|
+
*/
|
|
216
|
+
function createMotion() {
|
|
217
|
+
let started = false;
|
|
218
|
+
let revealObserver = null;
|
|
219
|
+
let countObserver = null;
|
|
220
|
+
let mutationObserver = null;
|
|
221
|
+
let scrollWatchers = [];
|
|
222
|
+
let rotateTimers = new Map();
|
|
223
|
+
let dotfields = new WeakSet();
|
|
224
|
+
let scrollHandler = null;
|
|
225
|
+
|
|
226
|
+
// ── reveals ────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
const markInview = (el) => el.setAttribute('data-omega-inview', 'true');
|
|
229
|
+
|
|
230
|
+
const observeReveal = (el) => {
|
|
231
|
+
if (el.hasAttribute('data-omega-inview')) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!revealObserver) {
|
|
235
|
+
markInview(el);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
revealObserver.observe(el);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const applyStagger = (parent) => {
|
|
242
|
+
const step = Number(parent.getAttribute('data-omega-reveal-stagger')) || 60;
|
|
243
|
+
parent.querySelectorAll('[data-omega-reveal]').forEach((el, index) => {
|
|
244
|
+
el.style.setProperty('--omega-reveal-delay', `${index * step}ms`);
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ── count-up ───────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
const runCountup = (el) => {
|
|
251
|
+
if (el.dataset.omegaCountupDone) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
el.dataset.omegaCountupDone = 'true';
|
|
255
|
+
|
|
256
|
+
const target = parseCountTarget(el.textContent);
|
|
257
|
+
if (!target || prefersReducedMotion()) {
|
|
258
|
+
return; // markup already shows the final value
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const startTime = performance.now();
|
|
262
|
+
const tick = (now) => {
|
|
263
|
+
const progress = Math.min((now - startTime) / COUNTUP_DURATION, 1);
|
|
264
|
+
const eased = 1 - ((1 - progress) ** 3);
|
|
265
|
+
el.textContent = formatCount(target, target.value * eased);
|
|
266
|
+
if (progress < 1) {
|
|
267
|
+
requestAnimationFrame(tick);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
el.textContent = formatCount(target, 0);
|
|
271
|
+
requestAnimationFrame(tick);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const observeCountup = (el) => {
|
|
275
|
+
if (el.dataset.omegaCountupDone) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!countObserver) {
|
|
279
|
+
runCountup(el);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
countObserver.observe(el);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// ── word rotator ───────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
const setupRotate = (el) => {
|
|
288
|
+
if (rotateTimers.has(el)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const words = Array.from(el.children);
|
|
293
|
+
if (!words.length) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let index = 0;
|
|
298
|
+
words.forEach((word, i) => word.toggleAttribute('data-omega-active', i === 0));
|
|
299
|
+
|
|
300
|
+
if (prefersReducedMotion() || words.length < 2) {
|
|
301
|
+
rotateTimers.set(el, null); // static first word, but marked as handled
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const interval = Number(el.getAttribute('data-omega-rotate')) || 2600;
|
|
306
|
+
const timer = setInterval(() => {
|
|
307
|
+
if (!el.isConnected) {
|
|
308
|
+
clearInterval(timer);
|
|
309
|
+
rotateTimers.delete(el);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
index = (index + 1) % words.length;
|
|
313
|
+
words.forEach((word, i) => word.toggleAttribute('data-omega-active', i === index));
|
|
314
|
+
}, interval);
|
|
315
|
+
rotateTimers.set(el, timer);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ── marquee ────────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
const setupMarquee = (el) => {
|
|
321
|
+
if (el.dataset.omegaMarqueeReady) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
el.dataset.omegaMarqueeReady = 'true';
|
|
325
|
+
|
|
326
|
+
if (prefersReducedMotion()) {
|
|
327
|
+
return; // CSS animation is off — leave the single static row
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const track = el.querySelector('.omega-marquee__track');
|
|
331
|
+
if (!track || !track.children.length) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const setItems = Array.from(track.children);
|
|
336
|
+
const speed = Number(el.getAttribute('data-omega-marquee')) || MARQUEE_SPEED;
|
|
337
|
+
|
|
338
|
+
// (Re)build: reset to the original set, clone it until half the track
|
|
339
|
+
// covers the container, then double that half so translateX(-50%) loops
|
|
340
|
+
// seamlessly. Duration scales with the half width → constant px/s. The
|
|
341
|
+
// measure waits for the NEXT frame: reading geometry in the tick that just
|
|
342
|
+
// replaced the children forces a synchronous reflow (#752).
|
|
343
|
+
const build = () => {
|
|
344
|
+
track.replaceChildren(...setItems);
|
|
345
|
+
|
|
346
|
+
requestAnimationFrame(() => {
|
|
347
|
+
if (track.children.length !== setItems.length) {
|
|
348
|
+
return; // a burst (resize, fonts, images) queued two: one rebuild wins
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const setWidth = track.getBoundingClientRect().width;
|
|
352
|
+
const copies = marqueeCopies(setWidth, el.clientWidth);
|
|
353
|
+
const total = copies * 2 * setItems.length;
|
|
354
|
+
|
|
355
|
+
while (track.children.length < total) {
|
|
356
|
+
const clone = setItems[track.children.length % setItems.length].cloneNode(true);
|
|
357
|
+
clone.setAttribute('aria-hidden', 'true');
|
|
358
|
+
// Clones stay mouse-clickable (they occupy most of the viewport as
|
|
359
|
+
// the track scrolls) but must not duplicate the tab order — so
|
|
360
|
+
// focusables get tabindex=-1, NOT inert.
|
|
361
|
+
const FOCUSABLE = 'a, button, input, select, textarea, [tabindex]';
|
|
362
|
+
if (clone.matches(FOCUSABLE)) {
|
|
363
|
+
clone.setAttribute('tabindex', '-1');
|
|
364
|
+
}
|
|
365
|
+
clone.querySelectorAll(FOCUSABLE).forEach(($focusable) => {
|
|
366
|
+
$focusable.setAttribute('tabindex', '-1');
|
|
367
|
+
});
|
|
368
|
+
track.appendChild(clone);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (setWidth > 0) {
|
|
372
|
+
track.style.setProperty('--omega-marquee-speed', `${Math.round((setWidth * copies) / speed)}s`);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
build();
|
|
378
|
+
|
|
379
|
+
// Re-run when geometry settles or changes: container resize, webfont
|
|
380
|
+
// swap, and images inside the set finishing their load.
|
|
381
|
+
if (typeof ResizeObserver === 'function') {
|
|
382
|
+
let lastWidth = el.clientWidth;
|
|
383
|
+
new ResizeObserver(() => {
|
|
384
|
+
if (el.clientWidth !== lastWidth) {
|
|
385
|
+
lastWidth = el.clientWidth;
|
|
386
|
+
build();
|
|
387
|
+
}
|
|
388
|
+
}).observe(el);
|
|
389
|
+
}
|
|
390
|
+
if (typeof document !== 'undefined') {
|
|
391
|
+
document.fonts?.ready?.then(() => build()).catch(() => {});
|
|
392
|
+
}
|
|
393
|
+
setItems.forEach((item) => {
|
|
394
|
+
item.querySelectorAll?.('img').forEach((img) => {
|
|
395
|
+
if (!img.complete) {
|
|
396
|
+
img.addEventListener('load', () => build(), { once: true });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// ── segmented controls ─────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
const setupSegmented = (el) => {
|
|
405
|
+
if (el.dataset.omegaSegmentedReady) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const doc = el.ownerDocument;
|
|
410
|
+
const thumb = doc.createElement('span');
|
|
411
|
+
thumb.className = 'omega-segmented__thumb';
|
|
412
|
+
thumb.setAttribute('aria-hidden', 'true');
|
|
413
|
+
el.prepend(thumb);
|
|
414
|
+
el.dataset.omegaSegmentedReady = 'true';
|
|
415
|
+
|
|
416
|
+
const position = () => {
|
|
417
|
+
const active = el.querySelector('.btn-check:checked + *')
|
|
418
|
+
|| el.querySelector('.active, [aria-pressed="true"]');
|
|
419
|
+
if (!active || !active.offsetWidth) {
|
|
420
|
+
thumb.style.opacity = '0';
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
thumb.style.opacity = '';
|
|
424
|
+
thumb.style.setProperty('--omega-segment-x', `${active.offsetLeft}px`);
|
|
425
|
+
thumb.style.setProperty('--omega-segment-y', `${active.offsetTop}px`);
|
|
426
|
+
thumb.style.setProperty('--omega-segment-w', `${active.offsetWidth}px`);
|
|
427
|
+
thumb.style.setProperty('--omega-segment-h', `${active.offsetHeight}px`);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// First position lands without a glide-in from 0,0
|
|
431
|
+
thumb.style.transition = 'none';
|
|
432
|
+
position();
|
|
433
|
+
requestAnimationFrame(() => {
|
|
434
|
+
thumb.style.transition = '';
|
|
435
|
+
position();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// User input, programmatic .active/aria-pressed stamps (tab JS, platform
|
|
439
|
+
// detection, appearance boot), geometry changes — all re-position.
|
|
440
|
+
const deferred = () => requestAnimationFrame(position);
|
|
441
|
+
el.addEventListener('click', deferred);
|
|
442
|
+
el.addEventListener('change', deferred);
|
|
443
|
+
if (typeof MutationObserver === 'function') {
|
|
444
|
+
new MutationObserver(position).observe(el, {
|
|
445
|
+
attributes: true,
|
|
446
|
+
subtree: true,
|
|
447
|
+
attributeFilter: ['class', 'aria-pressed', 'aria-selected'],
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (typeof ResizeObserver === 'function') {
|
|
451
|
+
new ResizeObserver(position).observe(el);
|
|
452
|
+
}
|
|
453
|
+
doc.fonts?.ready?.then(position).catch(() => {});
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// ── dot field ──────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
const setupDotfield = (el) => {
|
|
459
|
+
// Installed-here (this engine) or stamped-there (a previous one): the
|
|
460
|
+
// handoff stamp now waits for the first painted grid, so it cannot double
|
|
461
|
+
// as the re-entry guard between the scan and the settle signal.
|
|
462
|
+
if (dotfields.has(el) || el.dataset.omegaDotfieldReady) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const reduced = prefersReducedMotion();
|
|
467
|
+
const doc = el.ownerDocument;
|
|
468
|
+
const canvas = doc.createElement('canvas');
|
|
469
|
+
canvas.className = 'omega-dotfield__canvas';
|
|
470
|
+
canvas.setAttribute('aria-hidden', 'true');
|
|
471
|
+
const ctx = canvas.getContext?.('2d');
|
|
472
|
+
if (!ctx) {
|
|
473
|
+
return; // no canvas support — the static CSS dot grid stays
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
dotfields.add(el);
|
|
477
|
+
el.prepend(canvas);
|
|
478
|
+
|
|
479
|
+
const spacing = Number(el.getAttribute('data-omega-dotfield')) || 22;
|
|
480
|
+
const colors = { base: [128, 128, 128] };
|
|
481
|
+
const pointer = { x: -1e4, y: -1e4, targetX: -1e4, targetY: -1e4 };
|
|
482
|
+
let width = 0;
|
|
483
|
+
let height = 0;
|
|
484
|
+
let dpr = 1;
|
|
485
|
+
let running = false;
|
|
486
|
+
let settled = false;
|
|
487
|
+
let raf = 0;
|
|
488
|
+
let painted = false;
|
|
489
|
+
let lastColorRead = 0;
|
|
490
|
+
let lastPaint = -Infinity;
|
|
491
|
+
|
|
492
|
+
const readColors = () => {
|
|
493
|
+
lastColorRead = performance.now();
|
|
494
|
+
const styles = window.getComputedStyle(el);
|
|
495
|
+
colors.base = parseColor(ctx, styles.getPropertyValue('--omega-line-strong')) || colors.base;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const resize = () => {
|
|
499
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
500
|
+
width = el.clientWidth;
|
|
501
|
+
height = el.clientHeight;
|
|
502
|
+
canvas.width = Math.round(width * dpr);
|
|
503
|
+
canvas.height = Math.round(height * dpr);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const WAVE_LENGTH = (Math.PI * 2) / 900; // ~900px crest-to-crest diagonal
|
|
507
|
+
const POINTER_RADIUS = 2 * 110 * 110; // gaussian falloff (~110px reach)
|
|
508
|
+
|
|
509
|
+
// One grid, at time `now` (seconds of wave travel). The whole cost of the
|
|
510
|
+
// field lives here (every dot is an arc fill), so the loop below rations
|
|
511
|
+
// it rather than running it on every frame the display offers.
|
|
512
|
+
const paint = (now) => {
|
|
513
|
+
// Theme flips (data-bs-theme) land within a second
|
|
514
|
+
if (now - lastColorRead > 1000) {
|
|
515
|
+
readColors();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
519
|
+
ctx.clearRect(0, 0, width, height);
|
|
520
|
+
|
|
521
|
+
const [br, bg, bb] = colors.base;
|
|
522
|
+
const t = now / 1000;
|
|
523
|
+
const rgb = [0, 0, 0]; // reused per dot — no per-frame allocation churn
|
|
524
|
+
|
|
525
|
+
for (let y = spacing / 2; y < height; y += spacing) {
|
|
526
|
+
for (let x = spacing / 2; x < width; x += spacing) {
|
|
527
|
+
// Slow diagonal wave sweeping the grid; pointer adds a local lift
|
|
528
|
+
const wave = 0.5 + 0.5 * Math.sin((x + y * 0.6) * WAVE_LENGTH - t * 0.7);
|
|
529
|
+
const dx = x - pointer.x;
|
|
530
|
+
const dy = y - pointer.y;
|
|
531
|
+
const boost = Math.exp(-(dx * dx + dy * dy) / POINTER_RADIUS);
|
|
532
|
+
|
|
533
|
+
rainbowColor(x, y, width, t, rgb);
|
|
534
|
+
const mix = Math.min(1, wave * 0.45 + boost * 0.55);
|
|
535
|
+
const alpha = Math.min(1, 0.28 + wave * 0.38 + boost * 0.5);
|
|
536
|
+
const radius = 1 + wave * 0.35 + boost * 0.9;
|
|
537
|
+
|
|
538
|
+
ctx.fillStyle = `rgba(${Math.round(br + (rgb[0] - br) * mix)},${Math.round(bg + (rgb[1] - bg) * mix)},${Math.round(bb + (rgb[2] - bb) * mix)},${alpha})`;
|
|
539
|
+
ctx.beginPath();
|
|
540
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
541
|
+
ctx.fill();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!painted) {
|
|
546
|
+
painted = true;
|
|
547
|
+
// The CSS fallback dots (.omega-dotgrid::before) fade out on this
|
|
548
|
+
// stamp, so it lands with the grid that replaces them and never
|
|
549
|
+
// before: at scan it left the hero backdrop blank until settle (#752).
|
|
550
|
+
el.dataset.omegaDotfieldReady = 'true';
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const draw = (now) => {
|
|
555
|
+
raf = 0;
|
|
556
|
+
if (!running) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
raf = requestAnimationFrame(draw);
|
|
560
|
+
|
|
561
|
+
// Ease the pointer toward its target for a soft trail. Every frame, so
|
|
562
|
+
// the trail keeps its feel while the grid repaints at half the rate.
|
|
563
|
+
pointer.x += (pointer.targetX - pointer.x) * 0.12;
|
|
564
|
+
pointer.y += (pointer.targetY - pointer.y) * 0.12;
|
|
565
|
+
|
|
566
|
+
// The wave crawls (~900px crest, a 20s rainbow cycle), so 30fps reads
|
|
567
|
+
// exactly like 60 and costs half the main thread. The slop absorbs rAF
|
|
568
|
+
// jitter: without it a 33.3ms budget missed by a hair halves to 20fps.
|
|
569
|
+
if (now - lastPaint < DOTFIELD_FRAME - DOTFIELD_FRAME_SLOP) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
lastPaint = now;
|
|
573
|
+
paint(now);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const startLoop = () => {
|
|
577
|
+
if (settled && running && !raf) {
|
|
578
|
+
raf = requestAnimationFrame(draw);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const setRunning = (on) => {
|
|
583
|
+
if (on === running) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
running = on;
|
|
587
|
+
startLoop();
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// Nothing measures, reads styles or paints until first paint has settled:
|
|
591
|
+
// an ambient field is never worth a frame of the LCP window (#752).
|
|
592
|
+
whenSettled(doc, () => {
|
|
593
|
+
settled = true;
|
|
594
|
+
readColors();
|
|
595
|
+
resize();
|
|
596
|
+
|
|
597
|
+
if (reduced) {
|
|
598
|
+
paint(0); // the wave held at its start: one grid, and no loop
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
startLoop();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (reduced) {
|
|
606
|
+
// A visitor who asked for stillness gets the field, still: no pointer
|
|
607
|
+
// tracking, no visibility loop. It repaints only when what it draws
|
|
608
|
+
// changes, and re-reads its color there: the animated path's 1s color
|
|
609
|
+
// poll lives in the loop this path does not have.
|
|
610
|
+
const restill = (measure) => {
|
|
611
|
+
if (!settled) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (measure) {
|
|
615
|
+
resize();
|
|
616
|
+
}
|
|
617
|
+
readColors();
|
|
618
|
+
paint(0);
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
if (typeof ResizeObserver === 'function') {
|
|
622
|
+
new ResizeObserver(() => restill(true)).observe(el);
|
|
623
|
+
}
|
|
624
|
+
if (typeof MutationObserver === 'function') {
|
|
625
|
+
// Bound to documentElement, which outlives the field: let go the
|
|
626
|
+
// moment the field leaves the DOM, or a detached canvas repaints on
|
|
627
|
+
// every theme flip for the life of the page.
|
|
628
|
+
const themeObserver = new MutationObserver(() => {
|
|
629
|
+
if (el.isConnected === false) {
|
|
630
|
+
themeObserver.disconnect();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
restill(false);
|
|
634
|
+
});
|
|
635
|
+
themeObserver.observe(doc.documentElement, {
|
|
636
|
+
attributes: true,
|
|
637
|
+
attributeFilter: ['data-bs-theme'],
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Window-level tracking: an el-level pointermove stops firing while the
|
|
644
|
+
// cursor rides a covering element (the fixed nav over a hero), so the
|
|
645
|
+
// boost froze there and JUMPED on re-entry. The window always fires;
|
|
646
|
+
// out-of-range coordinates park the boost off-field smoothly.
|
|
647
|
+
const POINTER_MARGIN = 140;
|
|
648
|
+
window.addEventListener('pointermove', (event) => {
|
|
649
|
+
if (!running) {
|
|
650
|
+
return; // offscreen fields skip the rect read
|
|
651
|
+
}
|
|
652
|
+
const rect = el.getBoundingClientRect();
|
|
653
|
+
const x = event.clientX - rect.left;
|
|
654
|
+
const y = event.clientY - rect.top;
|
|
655
|
+
if (x < -POINTER_MARGIN || y < -POINTER_MARGIN
|
|
656
|
+
|| x > rect.width + POINTER_MARGIN || y > rect.height + POINTER_MARGIN) {
|
|
657
|
+
pointer.targetX = -1e4;
|
|
658
|
+
pointer.targetY = -1e4;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
pointer.targetX = x;
|
|
662
|
+
pointer.targetY = y;
|
|
663
|
+
}, { passive: true });
|
|
664
|
+
doc.documentElement.addEventListener('mouseleave', () => {
|
|
665
|
+
pointer.targetX = -1e4;
|
|
666
|
+
pointer.targetY = -1e4;
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
if (typeof ResizeObserver === 'function') {
|
|
670
|
+
new ResizeObserver(() => {
|
|
671
|
+
if (settled) {
|
|
672
|
+
resize(); // before settle the canvas is sized by whenSettled, not here
|
|
673
|
+
}
|
|
674
|
+
}).observe(el);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Only draw while on screen and the tab is visible
|
|
678
|
+
if (typeof IntersectionObserver === 'function') {
|
|
679
|
+
new IntersectionObserver((entries) => {
|
|
680
|
+
entries.forEach((entry) => setRunning(entry.isIntersecting && !doc.hidden));
|
|
681
|
+
}).observe(el);
|
|
682
|
+
} else {
|
|
683
|
+
setRunning(true);
|
|
684
|
+
}
|
|
685
|
+
doc.addEventListener('visibilitychange', () => {
|
|
686
|
+
if (doc.hidden) {
|
|
687
|
+
setRunning(false);
|
|
688
|
+
} else if (el.isConnected) {
|
|
689
|
+
setRunning(true);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// ── autoplaying video ──────────────────────────────────────────────────────
|
|
695
|
+
|
|
696
|
+
const setupVideo = (el) => {
|
|
697
|
+
if (el.dataset.omegaVideoReady) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
el.dataset.omegaVideoReady = 'true';
|
|
701
|
+
|
|
702
|
+
if (!prefersReducedMotion()) {
|
|
703
|
+
return; // the authored playback stands
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// CSS can park an animation; it cannot pause a <video>. So the engine
|
|
707
|
+
// does: the poster (or first frame) holds, the controls come on, and the
|
|
708
|
+
// visitor decides whether anything moves. One lane for every autoplaying
|
|
709
|
+
// band — no section forks its own.
|
|
710
|
+
el.removeAttribute('autoplay');
|
|
711
|
+
el.autoplay = false;
|
|
712
|
+
el.controls = true;
|
|
713
|
+
el.pause?.();
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// ── scroll watch ───────────────────────────────────────────────────────────
|
|
717
|
+
|
|
718
|
+
const syncScrollWatchers = () => {
|
|
719
|
+
const y = window.scrollY;
|
|
720
|
+
scrollWatchers = scrollWatchers.filter((entry) => entry.el.isConnected);
|
|
721
|
+
scrollWatchers.forEach((entry) => {
|
|
722
|
+
entry.el.setAttribute('data-omega-scrolled', y > entry.threshold ? 'true' : 'false');
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const setupScrollWatch = (el) => {
|
|
727
|
+
if (scrollWatchers.some((entry) => entry.el === el)) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
scrollWatchers.push({ el, threshold: Number(el.getAttribute('data-omega-scroll-watch')) || 24 });
|
|
731
|
+
syncScrollWatchers();
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// ── scan ───────────────────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Wire every motion attribute under (and on) a root. Idempotent.
|
|
738
|
+
* @param {Element|Document} root
|
|
739
|
+
*/
|
|
740
|
+
const scan = (root) => {
|
|
741
|
+
if (!root || !root.querySelectorAll) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const all = (selector) => {
|
|
746
|
+
const found = Array.from(root.querySelectorAll(selector));
|
|
747
|
+
if (root.matches?.(selector)) {
|
|
748
|
+
found.unshift(root);
|
|
749
|
+
}
|
|
750
|
+
return found;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
all('[data-omega-reveal-stagger]').forEach(applyStagger);
|
|
754
|
+
all('[data-omega-reveal]').forEach(observeReveal);
|
|
755
|
+
all('[data-omega-countup]').forEach(observeCountup);
|
|
756
|
+
all('[data-omega-rotate]').forEach(setupRotate);
|
|
757
|
+
all('[data-omega-marquee]').forEach(setupMarquee);
|
|
758
|
+
all('[data-omega-scroll-watch]').forEach(setupScrollWatch);
|
|
759
|
+
all('video[autoplay]').forEach(setupVideo);
|
|
760
|
+
all('[data-omega-segmented]').forEach(setupSegmented);
|
|
761
|
+
all('[data-omega-dotfield]').forEach(setupDotfield);
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Scan the document and observe it for inserted motion targets. Idempotent.
|
|
766
|
+
* @param {Document} [doc] - Defaults to the global document.
|
|
767
|
+
*/
|
|
768
|
+
const start = (doc = typeof document !== 'undefined' ? document : null) => {
|
|
769
|
+
if (!doc || started) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
started = true;
|
|
773
|
+
|
|
774
|
+
const begin = () => {
|
|
775
|
+
if (typeof IntersectionObserver === 'function' && !prefersReducedMotion()) {
|
|
776
|
+
revealObserver = new IntersectionObserver((entries) => {
|
|
777
|
+
entries.forEach((entry) => {
|
|
778
|
+
if (entry.isIntersecting) {
|
|
779
|
+
markInview(entry.target);
|
|
780
|
+
revealObserver.unobserve(entry.target);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}, { threshold: 0.1, rootMargin: '0px 0px -8% 0px' });
|
|
784
|
+
|
|
785
|
+
countObserver = new IntersectionObserver((entries) => {
|
|
786
|
+
entries.forEach((entry) => {
|
|
787
|
+
if (entry.isIntersecting) {
|
|
788
|
+
runCountup(entry.target);
|
|
789
|
+
countObserver.unobserve(entry.target);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}, { threshold: 0.4 });
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
scrollHandler = (() => {
|
|
796
|
+
let ticking = false;
|
|
797
|
+
return () => {
|
|
798
|
+
if (ticking) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
ticking = true;
|
|
802
|
+
requestAnimationFrame(() => {
|
|
803
|
+
syncScrollWatchers();
|
|
804
|
+
ticking = false;
|
|
805
|
+
});
|
|
806
|
+
};
|
|
807
|
+
})();
|
|
808
|
+
window.addEventListener('scroll', scrollHandler, { passive: true });
|
|
809
|
+
|
|
810
|
+
if (typeof MutationObserver === 'function') {
|
|
811
|
+
mutationObserver = new MutationObserver((mutations) => {
|
|
812
|
+
for (const mutation of mutations) {
|
|
813
|
+
mutation.addedNodes.forEach((node) => {
|
|
814
|
+
if (node.nodeType === 1) {
|
|
815
|
+
scan(node);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
mutationObserver.observe(doc.documentElement, { childList: true, subtree: true });
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
scan(doc);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
if (doc.documentElement && doc.readyState !== 'loading') {
|
|
827
|
+
begin();
|
|
828
|
+
} else {
|
|
829
|
+
doc.addEventListener('DOMContentLoaded', begin, { once: true });
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const stop = () => {
|
|
834
|
+
revealObserver?.disconnect();
|
|
835
|
+
countObserver?.disconnect();
|
|
836
|
+
mutationObserver?.disconnect();
|
|
837
|
+
revealObserver = null;
|
|
838
|
+
countObserver = null;
|
|
839
|
+
mutationObserver = null;
|
|
840
|
+
if (scrollHandler) {
|
|
841
|
+
window.removeEventListener('scroll', scrollHandler);
|
|
842
|
+
scrollHandler = null;
|
|
843
|
+
}
|
|
844
|
+
rotateTimers.forEach((timer) => timer && clearInterval(timer));
|
|
845
|
+
rotateTimers = new Map();
|
|
846
|
+
scrollWatchers = [];
|
|
847
|
+
started = false;
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
return { start, stop, scan };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
module.exports = { createMotion, parseCountTarget, formatCount, marqueeCopies, parseColor, rainbowColor };
|