gclass-anims 1.0.0-beta.21 → 1.0.0-beta.22
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/AnimToggle.js +1 -1
- package/Animations.js +71 -17
- package/CHANGELOG.md +8 -0
- package/Config.js +1 -0
- package/CustomAnims.js +1 -1
- package/Listeners.js +265 -68
- package/dist/gclass.cjs +3268 -0
- package/dist/gclass.esm.js +3208 -0
- package/package.json +12 -5
package/Listeners.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import gsap from 'gsap'
|
|
1
|
+
import { gsap } from 'gsap'
|
|
2
2
|
import { SpawnV, verticalmove, expandmove, magnet, magnet3d, reset, typewriter, countTargetVars, stashText, scrambleVars } from './Animations.js'
|
|
3
3
|
import { customAnims } from './CustomAnims.js'
|
|
4
4
|
import { defaults, normalize } from './Config.js'
|
|
@@ -12,7 +12,94 @@ const TEXT_PREFIX_LEN = TEXT_PREFIX.length
|
|
|
12
12
|
// The engine is fully config-driven. All animation definitions live in
|
|
13
13
|
// Config.js; here we just normalise them into the two internal views the
|
|
14
14
|
// machinery consumes (spawn/entrance + loop) plus the raw `all` list.
|
|
15
|
-
|
|
15
|
+
// NOTE: normalized inside initListeners per-call (see beta.22) so runtime
|
|
16
|
+
// customAnims.push() before next init is picked up.
|
|
17
|
+
|
|
18
|
+
// --- Breakpoint support (non-conflicting with Tailwind/Bootstrap) -----------
|
|
19
|
+
// defaults.breakpoints = {xs:475, s:640, m:768, l:1024, xl:1280} -> min-width
|
|
20
|
+
// Single-letter s/m/l avoids collision with Tailwind's sm/md/lg. Usage:
|
|
21
|
+
// <div class="spawn-up"> always
|
|
22
|
+
// <div class="m:spawn-up"> from 768px up
|
|
23
|
+
// <div class="l:spawn-up"> from 1024px up
|
|
24
|
+
// <div class="xs:spawn-up"> from 475px up
|
|
25
|
+
// Colon is valid in classList (class="m:spawn-up") and checked via
|
|
26
|
+
// classList.contains - never via unescaped querySelector (":pseudo" would break).
|
|
27
|
+
const bpEntries = Object.entries(defaults.breakpoints || {}).sort((a, b) => a[1] - b[1])
|
|
28
|
+
const bpNames = bpEntries.map(([k]) => k)
|
|
29
|
+
const bpMap = Object.fromEntries(bpEntries)
|
|
30
|
+
const bpPrefixRE = bpNames.length ? new RegExp(`^(${bpNames.join('|')}):(.+)$`) : /^$^/
|
|
31
|
+
const isBreakpointActive = (bp) => {
|
|
32
|
+
if (!bp) return true
|
|
33
|
+
const px = bpMap[bp]
|
|
34
|
+
if (px == null) return true
|
|
35
|
+
if (typeof window === 'undefined' || !window.matchMedia) return true
|
|
36
|
+
return window.matchMedia(`(min-width: ${px}px)`).matches
|
|
37
|
+
}
|
|
38
|
+
const mqForBp = (bp) => `(min-width: ${bpMap[bp]}px)`
|
|
39
|
+
// Does el carry `bp:base` and is that bp currently active? Also handles base without prefix.
|
|
40
|
+
const elementMatchesSel = (el, sel) => {
|
|
41
|
+
const base = sel.slice(1) // ".spawn-up" -> "spawn-up"
|
|
42
|
+
if (el.classList.contains(base)) return true
|
|
43
|
+
for (const bp of bpNames) {
|
|
44
|
+
if (el.classList.contains(`${bp}:${base}`) && isBreakpointActive(bp)) return true
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
const hasGClass = (el, name) => {
|
|
49
|
+
if (el.classList.contains(name)) return true
|
|
50
|
+
for (const bp of bpNames) if (el.classList.contains(`${bp}:${name}`) && isBreakpointActive(bp)) return true
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
// If el has ANY bp:* class, it is breakpoint-gated. When gated, require at least one active variant.
|
|
54
|
+
const isElBreakpointActive = (el) => {
|
|
55
|
+
let hasBp = false
|
|
56
|
+
for (const c of el.classList) {
|
|
57
|
+
const m = c.match(bpPrefixRE)
|
|
58
|
+
if (!m) continue
|
|
59
|
+
hasBp = true
|
|
60
|
+
if (isBreakpointActive(m[1])) return true
|
|
61
|
+
}
|
|
62
|
+
return !hasBp // no bp prefix -> always active
|
|
63
|
+
}
|
|
64
|
+
// Collect all elements matching sel OR its bp variants that are currently active.
|
|
65
|
+
// Uses manual classList scan instead of querySelector(".m\\:spawn-up") to avoid escaping issues.
|
|
66
|
+
const qAllBp = (sel, qAll) => {
|
|
67
|
+
const base = sel.slice(1)
|
|
68
|
+
const all = qAll("body *")
|
|
69
|
+
return all.filter(el => elementMatchesSel(el, sel) || (() => {
|
|
70
|
+
// also handle spawn-text-* where TEXT_PREFIX is retained: sel=".spawn-text-spawn-up" -> base="spawn-text-spawn-up"
|
|
71
|
+
// already covered by elementMatchesSel
|
|
72
|
+
return false
|
|
73
|
+
})())
|
|
74
|
+
}
|
|
75
|
+
// For generic qAll(sel) calls that should be breakpoint-aware, use this wrapper.
|
|
76
|
+
const wrapQAll = (qAll) => (sel) => {
|
|
77
|
+
// if sel is a comma list (orderSelector), split and union
|
|
78
|
+
if (sel.includes(',')) {
|
|
79
|
+
const parts = sel.split(',').map(s => s.trim()).filter(Boolean)
|
|
80
|
+
const set = new Set()
|
|
81
|
+
parts.forEach(p => wrapQAll(qAll)(p).forEach(e => set.add(e)))
|
|
82
|
+
return [...set]
|
|
83
|
+
}
|
|
84
|
+
// "body *" passthrough
|
|
85
|
+
if (sel === "body *") return qAll(sel)
|
|
86
|
+
const hasBpInSel = bpNames.some(bp => sel.includes(`${bp}:`))
|
|
87
|
+
if (hasBpInSel) {
|
|
88
|
+
// sel already contains bp: variant - filter by active
|
|
89
|
+
return qAll("body *").filter(el => sel.split(',').some(s => elementMatchesSel(el, s.trim())))
|
|
90
|
+
}
|
|
91
|
+
// normal sel - include base + active variants
|
|
92
|
+
try {
|
|
93
|
+
const baseEls = qAll(sel)
|
|
94
|
+
// also scan for active variants that wouldn't be found by qAll(sel)
|
|
95
|
+
const variantEls = qAll("body *").filter(el => {
|
|
96
|
+
for (const bp of bpNames) if (el.classList.contains(`${bp}:${sel.slice(1)}`) && isBreakpointActive(bp)) return true
|
|
97
|
+
return false
|
|
98
|
+
})
|
|
99
|
+
const set = new Set([...baseEls, ...variantEls])
|
|
100
|
+
return [...set]
|
|
101
|
+
} catch { return qAll(sel) }
|
|
102
|
+
}
|
|
16
103
|
|
|
17
104
|
// --- Named onComplete handler registry -------------------------------------
|
|
18
105
|
// `on-<kind>-complete-<name>` classes resolve `<name>` to a function here
|
|
@@ -101,6 +188,8 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
101
188
|
}
|
|
102
189
|
throttlePerFrame = Number(throttlePerFrame) || 0 // 0 = no throttling (default)
|
|
103
190
|
gsap.registerPlugin(TextPlugin, ScrollTrigger, SplitText)
|
|
191
|
+
// Re-normalize per init so runtime customAnims.push() is picked up (beta.22)
|
|
192
|
+
const { all: animAll, spawnConfigs, loopConfigs } = normalize(customAnims)
|
|
104
193
|
|
|
105
194
|
// helper to scope queries to root (for boot screen: only boot-up subtree animates during boot)
|
|
106
195
|
const qAll = (sel) => {
|
|
@@ -113,6 +202,44 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
113
202
|
return els
|
|
114
203
|
} catch { return gsap.utils.toArray(sel) }
|
|
115
204
|
}
|
|
205
|
+
// breakpoint-aware helpers (s/m/l/xl) - s/m/l avoids Tailwind sm/md/lg collision
|
|
206
|
+
const mm = gsap.matchMedia()
|
|
207
|
+
const breakpointContexts = [] // track mm contexts for teardown
|
|
208
|
+
const getGateBpForSel = (el, sel) => {
|
|
209
|
+
const base = sel.slice(1)
|
|
210
|
+
for (const bp of bpNames) if (el.classList.contains(`${bp}:${base}`)) return bp
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
const getElGateBp = (el) => {
|
|
214
|
+
for (const c of el.classList) {
|
|
215
|
+
const m = c.match(bpPrefixRE)
|
|
216
|
+
if (m) return m[1]
|
|
217
|
+
}
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
const runWithBreakpoint = (el, fn) => {
|
|
221
|
+
const bp = getElGateBp(el)
|
|
222
|
+
if (!bp) { fn(); return }
|
|
223
|
+
const mq = mqForBp(bp)
|
|
224
|
+
const ret = mm.add(mq, fn)
|
|
225
|
+
breakpointContexts.push(ret)
|
|
226
|
+
}
|
|
227
|
+
const runWithBreakpointForSel = (el, sel, fn) => {
|
|
228
|
+
const bp = getGateBpForSel(el, sel)
|
|
229
|
+
if (!bp) { fn(); return }
|
|
230
|
+
const mq = mqForBp(bp)
|
|
231
|
+
const ret = mm.add(mq, fn)
|
|
232
|
+
breakpointContexts.push(ret)
|
|
233
|
+
}
|
|
234
|
+
// All elements that have base OR any bp:base (regardless of active) - for mm registration
|
|
235
|
+
const qAllAllVariants = (sel) => {
|
|
236
|
+
const base = sel.slice(1)
|
|
237
|
+
// include base + any variant
|
|
238
|
+
const all = qAll("body *")
|
|
239
|
+
return all.filter(el => el.classList.contains(base) || bpNames.some(bp => el.classList.contains(`${bp}:${base}`)))
|
|
240
|
+
}
|
|
241
|
+
// Active-only view (used for order calculations that must reflect current viewport)
|
|
242
|
+
const qAllActive = wrapQAll(qAll)
|
|
116
243
|
|
|
117
244
|
const registeredListeners = []
|
|
118
245
|
const onCompleteTweens = []
|
|
@@ -120,13 +247,41 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
120
247
|
el.addEventListener(type, fn)
|
|
121
248
|
registeredListeners.push({ el, type, fn })
|
|
122
249
|
}
|
|
250
|
+
// --- Breakpoint-aware modifiers (amount-N, time-N, priority-N, etc.) --------
|
|
251
|
+
// Supports `m:amount-20`, `l:time-2`, `xl:ease-bounce` etc.
|
|
252
|
+
// Mobile-first: larger active breakpoint wins over smaller/base.
|
|
253
|
+
// e.g. class="amount-10 m:amount-20 l:amount-30" -> 10@xs/s, 20@m, 30@l/xl
|
|
254
|
+
const getActivePrefixedClass = (el, prefix) => {
|
|
255
|
+
let best = null
|
|
256
|
+
let bestPx = -2
|
|
257
|
+
for (const c of el.classList) {
|
|
258
|
+
let bp = null
|
|
259
|
+
let core = c
|
|
260
|
+
const m = c.match(bpPrefixRE)
|
|
261
|
+
if (m) { bp = m[1]; core = m[2] }
|
|
262
|
+
if (!core.startsWith(prefix)) continue
|
|
263
|
+
if (bp && !isBreakpointActive(bp)) continue
|
|
264
|
+
const px = bp ? bpMap[bp] : -1 // base = -1, xs=475 etc.
|
|
265
|
+
if (px > bestPx) { best = c; bestPx = px; }
|
|
266
|
+
}
|
|
267
|
+
return best
|
|
268
|
+
}
|
|
269
|
+
const extractNumber = (cls, prefix) => {
|
|
270
|
+
const idx = cls.indexOf(prefix)
|
|
271
|
+
if (idx === -1) return NaN
|
|
272
|
+
return Number(cls.slice(idx + prefix.length))
|
|
273
|
+
}
|
|
123
274
|
const readClassNumber = (el, prefix, fallback) => {
|
|
124
|
-
const match =
|
|
125
|
-
|
|
275
|
+
const match = getActivePrefixedClass(el, prefix)
|
|
276
|
+
if (!match) return fallback
|
|
277
|
+
const n = extractNumber(match, prefix)
|
|
278
|
+
return Number.isNaN(n) ? fallback : n
|
|
126
279
|
}
|
|
127
280
|
const getEase = (el) => {
|
|
128
|
-
const match =
|
|
129
|
-
|
|
281
|
+
const match = getActivePrefixedClass(el, "ease-")
|
|
282
|
+
if (!match) return defaults.ease
|
|
283
|
+
const idx = match.indexOf("ease-")
|
|
284
|
+
return match.slice(idx + "ease-".length) || defaults.ease
|
|
130
285
|
}
|
|
131
286
|
|
|
132
287
|
// Reduced-motion support. `.reduced` is a per-element opt-out: when the
|
|
@@ -134,7 +289,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
134
289
|
// completely un-animated (its spawn/loop/click/scroll/setup all skip).
|
|
135
290
|
const reducedMotion = () =>
|
|
136
291
|
(typeof window !== "undefined" && window.matchMedia?.(`(prefers-reduced-motion: reduce)`)?.matches) ?? false
|
|
137
|
-
const isReduced = (el) => reducedMotion() && el
|
|
292
|
+
const isReduced = (el) => reducedMotion() && hasGClass(el, "reduced")
|
|
138
293
|
|
|
139
294
|
// `.preserve` keeps an already-rendered element (e.g. one that persists
|
|
140
295
|
// in a shared layout across route changes) from being re-animated when
|
|
@@ -177,11 +332,20 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
177
332
|
// Leave animations derive from spawnConfigs so adding an entry here
|
|
178
333
|
// automatically enables its leave/exit reverse too (single source of truth).
|
|
179
334
|
const findSpawn = (el) => {
|
|
180
|
-
const direct = spawnConfigs.find(({ sel }) => el
|
|
335
|
+
const direct = spawnConfigs.find(({ sel }) => elementMatchesSel(el, sel))
|
|
181
336
|
if (direct) return direct
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
337
|
+
// handle TEXT_PREFIX with optional bp: prefix (e.g. "m:spawn-text-spawn-up")
|
|
338
|
+
for (const c of el.classList) {
|
|
339
|
+
let base = c
|
|
340
|
+
let bp = null
|
|
341
|
+
const m = c.match(bpPrefixRE)
|
|
342
|
+
if (m) { bp = m[1]; base = m[2] }
|
|
343
|
+
if (!base.startsWith(TEXT_PREFIX)) continue
|
|
344
|
+
if (bp && !isBreakpointActive(bp)) continue
|
|
345
|
+
const found = spawnConfigs.find(({ sel }) => sel === "." + base.slice(TEXT_PREFIX_LEN))
|
|
346
|
+
if (found) return found
|
|
347
|
+
}
|
|
348
|
+
return null
|
|
185
349
|
}
|
|
186
350
|
|
|
187
351
|
const isGhost = (el) => el?.dataset?.gsapGhost === "1"
|
|
@@ -196,7 +360,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
196
360
|
const leaveStates = new WeakMap()
|
|
197
361
|
|
|
198
362
|
const captureLeave = (node) => {
|
|
199
|
-
if (!node
|
|
363
|
+
if (!hasGClass(node, "leave")) return
|
|
200
364
|
const config = findSpawn(node)
|
|
201
365
|
if (!config || config.typewriter) return
|
|
202
366
|
leaveStates.set(node, {
|
|
@@ -227,7 +391,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
227
391
|
// Capture the element's first (resting) bounds. A later layout change
|
|
228
392
|
// morphs from this snapshot to the live position - a vanilla FLIP.
|
|
229
393
|
const captureFlip = (node) => {
|
|
230
|
-
if (!node
|
|
394
|
+
if (!hasGClass(node, "flip")) return
|
|
231
395
|
const config = findSpawn(node)
|
|
232
396
|
if (!config || config.typewriter) return
|
|
233
397
|
if (flipping.has(node)) return
|
|
@@ -350,8 +514,14 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
350
514
|
|
|
351
515
|
const collectLeave = (node) => {
|
|
352
516
|
if (!node || node.nodeType !== 1) return []
|
|
353
|
-
if (node.classList
|
|
354
|
-
|
|
517
|
+
if (node.classList && hasGClass(node, "leave")) return [node]
|
|
518
|
+
// also consider bp:leave variants - fallback to manual filter
|
|
519
|
+
const found = gsap.utils.toArray(node.querySelectorAll?.(".leave") || [])
|
|
520
|
+
// add bp:leave matches
|
|
521
|
+
qAll("body *").filter(el => hasGClass(el, "leave") && node.contains?.(el) && !found.includes(el)).forEach(el => found.push(el))
|
|
522
|
+
// check for bp:leave on node itself via manual scan if not in found
|
|
523
|
+
if (hasGClass(node, "leave") && !found.includes(node)) found.unshift(node)
|
|
524
|
+
return found
|
|
355
525
|
}
|
|
356
526
|
|
|
357
527
|
// Refresh the cached rect to the element's RESTING position (after its
|
|
@@ -376,14 +546,14 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
376
546
|
spawnConfigs.map(({ sel }) => "." + TEXT_PREFIX + sel.slice(1)).join(",")
|
|
377
547
|
|
|
378
548
|
const getOrderDelay = (el, priority) => {
|
|
379
|
-
const samepri =
|
|
549
|
+
const samepri = qAllActive(orderSelector())
|
|
380
550
|
.filter((e) => {
|
|
381
|
-
if (!e
|
|
551
|
+
if (!hasGClass(e, "order")) return false
|
|
382
552
|
const match = [...e.classList].find(p => p.startsWith("priority-"))
|
|
383
553
|
return (match ? Number(match.split("-")[1]) : 0) === priority
|
|
384
554
|
})
|
|
385
555
|
let order = samepri.indexOf(el)
|
|
386
|
-
if (el
|
|
556
|
+
if (hasGClass(el, "reverse")) {
|
|
387
557
|
order = samepri.length - 1 - order
|
|
388
558
|
}
|
|
389
559
|
return order / defaults.orderDivide
|
|
@@ -392,7 +562,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
392
562
|
const readTiming = (el) => {
|
|
393
563
|
const priority = readClassNumber(el, "priority-", 0)
|
|
394
564
|
return {
|
|
395
|
-
delay: el
|
|
565
|
+
delay: hasGClass(el, "order")
|
|
396
566
|
? getOrderDelay(el, priority)
|
|
397
567
|
: priority * defaults.spawnDelayMultiplier,
|
|
398
568
|
duration: readClassNumber(el, "time-", 1),
|
|
@@ -465,9 +635,9 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
465
635
|
// default). Default (no class) is per-WORD: far fewer split nodes, so
|
|
466
636
|
// the per-part spawn is much cheaper to animate and paint.
|
|
467
637
|
const getGranularity = (el) => {
|
|
468
|
-
if (el
|
|
469
|
-
if (el
|
|
470
|
-
if (el
|
|
638
|
+
if (hasGClass(el, "lines")) return "lines"
|
|
639
|
+
if (hasGClass(el, "words")) return "words"
|
|
640
|
+
if (hasGClass(el, "letter")) return "chars"
|
|
471
641
|
return "words"
|
|
472
642
|
}
|
|
473
643
|
// Cursive RTL scripts (Arabic/Persian) render each letter as a distinct
|
|
@@ -673,7 +843,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
673
843
|
// finishes. Unless the author opts out with `.no-revert`, this frees the
|
|
674
844
|
// hundreds of per-letter elements so the browser stops reflowing them.
|
|
675
845
|
const revertSplit = (el) => {
|
|
676
|
-
if (el
|
|
846
|
+
if (hasGClass(el, "no-revert")) return
|
|
677
847
|
const s = splitCache.get(el)
|
|
678
848
|
if (!s) return
|
|
679
849
|
splitCache.delete(el)
|
|
@@ -722,7 +892,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
722
892
|
return gsap.fromTo(parts, effFrom, {
|
|
723
893
|
...computeTo(effFrom), ease, duration, delay, stagger,
|
|
724
894
|
onComplete: () => {
|
|
725
|
-
if (el
|
|
895
|
+
if (hasGClass(el, "leave")) refreshLeaveRect(el)
|
|
726
896
|
revertSplit(el)
|
|
727
897
|
fireOnComplete(el, "spawn")
|
|
728
898
|
},
|
|
@@ -758,7 +928,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
758
928
|
// classes are present (N = how many % into view to engage, and how far
|
|
759
929
|
// out of view to release), else the full `top top` -> `bottom bottom`.
|
|
760
930
|
const setupPin = (el) => {
|
|
761
|
-
if (!el
|
|
931
|
+
if (!hasGClass(el, "pin") || el.dataset.gsapPinned) return
|
|
762
932
|
const clamp = (n) => Math.min(100, Math.max(0, n))
|
|
763
933
|
const startClass = readClassNumber(el, "progress-start-", null)
|
|
764
934
|
const endClass = readClassNumber(el, "progress-end-", null)
|
|
@@ -780,7 +950,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
780
950
|
// created before any scroll/scroll-progress trigger measures its position.
|
|
781
951
|
// Setting them up here (before the trigger pass below) keeps offsets correct
|
|
782
952
|
// and lets the single ScrollTrigger.refresh() at the end reconcile layout.
|
|
783
|
-
|
|
953
|
+
qAllAllVariants(".pin").forEach(el => runWithBreakpointForSel(el, ".pin", () => setupPin(el)))
|
|
784
954
|
|
|
785
955
|
// Scroll-driven extras - class-driven ScrollTrigger behaviours that don't
|
|
786
956
|
// fit the spawn/loop machinery (no `play`/`build`), handled like `.pin`:
|
|
@@ -803,9 +973,9 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
803
973
|
const cls = [...el.classList]
|
|
804
974
|
const clamp = (n) => Math.min(100, Math.max(0, n))
|
|
805
975
|
|
|
806
|
-
const parallaxCls =
|
|
976
|
+
const parallaxCls = getActivePrefixedClass(el, "parallax-")
|
|
807
977
|
if (parallaxCls) {
|
|
808
|
-
const factor = parseFloat(parallaxCls.slice("parallax-".length)) || 1
|
|
978
|
+
const factor = parseFloat(parallaxCls.slice(parallaxCls.indexOf("parallax-") + "parallax-".length)) || 1
|
|
809
979
|
if (factor === 1) return
|
|
810
980
|
const amt = (factor - 1) * 50
|
|
811
981
|
const t = gsap.fromTo(el,
|
|
@@ -819,13 +989,13 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
819
989
|
return
|
|
820
990
|
}
|
|
821
991
|
|
|
822
|
-
if (el
|
|
992
|
+
if (hasGClass(el, "progress-bar") || hasGClass(el, "scroll-fill")) {
|
|
823
993
|
const startClass = readClassNumber(el, "progress-start-", null)
|
|
824
994
|
const endClass = readClassNumber(el, "progress-end-", null)
|
|
825
995
|
// `.progress-reverse` runs the fill in reverse (full -> empty).
|
|
826
996
|
// GSAP's ScrollTrigger has no `reversed` config; swap the from/to
|
|
827
997
|
// so the scrub maps in the opposite direction instead.
|
|
828
|
-
const reverse = el
|
|
998
|
+
const reverse = hasGClass(el, "progress-reverse")
|
|
829
999
|
const t = gsap.fromTo(el,
|
|
830
1000
|
{ scaleX: reverse ? 1 : 0 },
|
|
831
1001
|
{
|
|
@@ -842,7 +1012,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
842
1012
|
return
|
|
843
1013
|
}
|
|
844
1014
|
|
|
845
|
-
if (el
|
|
1015
|
+
if (hasGClass(el, "scroll-fade-bg")) {
|
|
846
1016
|
const t = gsap.fromTo(el,
|
|
847
1017
|
{ backgroundPosition: "0% 0%" },
|
|
848
1018
|
{
|
|
@@ -854,7 +1024,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
854
1024
|
return
|
|
855
1025
|
}
|
|
856
1026
|
|
|
857
|
-
if (el
|
|
1027
|
+
if (hasGClass(el, "scroll-horizontal")) {
|
|
858
1028
|
const track = el.querySelector(".scroll-track")
|
|
859
1029
|
if (!track) return
|
|
860
1030
|
const getAmount = () => track.scrollWidth - el.clientWidth
|
|
@@ -873,7 +1043,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
873
1043
|
scrollTriggers.push(t.scrollTrigger)
|
|
874
1044
|
}
|
|
875
1045
|
}
|
|
876
|
-
qAll(
|
|
1046
|
+
qAll("body *").filter(el => hasGClass(el,"progress-bar") || hasGClass(el,"scroll-fill") || hasGClass(el,"scroll-fade-bg") || hasGClass(el,"scroll-horizontal") || [...el.classList].some(c=>c.startsWith("parallax-") || bpNames.some(bp=>c.startsWith(`${bp}:parallax-`)))).forEach(el => setupScrollDriven(el))
|
|
877
1047
|
|
|
878
1048
|
// Scroller resolution: a `.scroll`/`.scroll-progress` element inside a
|
|
879
1049
|
// `.scroll-frame` container binds its trigger to THAT box instead of the
|
|
@@ -899,7 +1069,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
899
1069
|
if (!config) return
|
|
900
1070
|
const { from, typewriter: isTypewriter, typewriterSplit, play } = config
|
|
901
1071
|
|
|
902
|
-
if (el
|
|
1072
|
+
if (hasGClass(el, "scroll-progress")) {
|
|
903
1073
|
const ease = isTypewriter
|
|
904
1074
|
? ([...el.classList].find(c => c.startsWith("ease-"))?.split("-")[1] ?? "none")
|
|
905
1075
|
: getEase(el)
|
|
@@ -920,7 +1090,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
920
1090
|
// `.progress-reverse` runs the scrub in reverse (revealed -> hidden).
|
|
921
1091
|
// GSAP's ScrollTrigger ignores a `reversed` config; swap from/to so
|
|
922
1092
|
// the scrub maps in the opposite direction instead.
|
|
923
|
-
const reverse = el
|
|
1093
|
+
const reverse = hasGClass(el, "progress-reverse")
|
|
924
1094
|
// .randomize-* applies to the HIDDEN start only (non-reverse):
|
|
925
1095
|
// a scrub's resting end must stay deterministic or the element
|
|
926
1096
|
// would sit permanently off-pose after being scrolled through.
|
|
@@ -966,7 +1136,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
966
1136
|
} else if (typewriterSplit) {
|
|
967
1137
|
const parts = getParts(el, getGranularity(el))
|
|
968
1138
|
if (parts.length) tl.fromTo(parts, { opacity: reverse ? 1 : 0 }, { opacity: reverse ? 0 : 1, ease })
|
|
969
|
-
} else if (el
|
|
1139
|
+
} else if (hasGClass(el, "fill-svg") && (hasGClass(el, "draw") || hasGClass(el, "draw-split"))) {
|
|
970
1140
|
// fill-svg modifier for draw: stroke first, then fill, sequential scrub.
|
|
971
1141
|
// For scrub the two phases are sequential tweens so scroll maps
|
|
972
1142
|
// draw → fill. Reverse swaps order so unfill happens before undraw.
|
|
@@ -991,7 +1161,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
991
1161
|
return
|
|
992
1162
|
}
|
|
993
1163
|
|
|
994
|
-
if (!el
|
|
1164
|
+
if (!hasGClass(el, "scroll")) return
|
|
995
1165
|
const { delay, duration } = readTiming(el)
|
|
996
1166
|
const ease = isTypewriter
|
|
997
1167
|
? ([...el.classList].find(c => c.startsWith("ease-"))?.split("-")[1] ?? "none")
|
|
@@ -1042,7 +1212,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1042
1212
|
})
|
|
1043
1213
|
scrollTriggers.push(st)
|
|
1044
1214
|
}
|
|
1045
|
-
qAll(".scroll,
|
|
1215
|
+
qAll("body *").filter(el => hasGClass(el, "scroll") || hasGClass(el, "scroll-progress")).forEach(el => runWithBreakpoint(el, () => setupScroll(el)))
|
|
1046
1216
|
|
|
1047
1217
|
// SplitText scroll variants: `.spawn-text-<spawn>.scroll` plays the per-part
|
|
1048
1218
|
// tween when the element enters the viewport and reverses on exit.
|
|
@@ -1050,7 +1220,8 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1050
1220
|
if (isTypewriter || text === false) return
|
|
1051
1221
|
const tSel = "." + TEXT_PREFIX + sel.slice(1)
|
|
1052
1222
|
|
|
1053
|
-
qAll(tSel
|
|
1223
|
+
qAll("body *").filter(el => elementMatchesSel(el, tSel) && hasGClass(el, "scroll") && !hasGClass(el, "scroll-progress")).forEach((el) => {
|
|
1224
|
+
const run = () => {
|
|
1054
1225
|
if (isReduced(el)) return
|
|
1055
1226
|
const { delay, duration } = readTiming(el)
|
|
1056
1227
|
const ease = getEase(el)
|
|
@@ -1073,9 +1244,11 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1073
1244
|
onLeave: reverseToStart,
|
|
1074
1245
|
onLeaveBack: reverseToStart,
|
|
1075
1246
|
}))
|
|
1247
|
+
}; runWithBreakpoint(el, run)
|
|
1076
1248
|
})
|
|
1077
1249
|
|
|
1078
|
-
qAll(tSel
|
|
1250
|
+
qAll("body *").filter(el => elementMatchesSel(el, tSel) && hasGClass(el, "scroll-progress")).forEach((el) => {
|
|
1251
|
+
const run = () => {
|
|
1079
1252
|
if (isReduced(el)) return
|
|
1080
1253
|
const ease = getEase(el)
|
|
1081
1254
|
const parts = getParts(el, getGranularity(el))
|
|
@@ -1096,11 +1269,12 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1096
1269
|
// `.progress-reverse` runs the split scrub in reverse; swap from/to.
|
|
1097
1270
|
// Randomize applies to the hidden (non-reverse) start only, same
|
|
1098
1271
|
// rule as the element-level scrub above.
|
|
1099
|
-
const reverse = el
|
|
1272
|
+
const reverse = hasGClass(el, "progress-reverse")
|
|
1100
1273
|
const rnd = !reverse && hasRandom(el) ? randomVars(el) : null
|
|
1101
1274
|
tl.fromTo(parts, { ...(reverse ? to : from), ...rnd },
|
|
1102
1275
|
{ ...(rnd ? randomEnds(Object.keys(rnd)) : null), ...(reverse ? from : to), ease })
|
|
1103
1276
|
scrollTriggers.push(tl.scrollTrigger)
|
|
1277
|
+
}; runWithBreakpoint(el, run)
|
|
1104
1278
|
})
|
|
1105
1279
|
})
|
|
1106
1280
|
ScrollTrigger.refresh()
|
|
@@ -1128,14 +1302,18 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1128
1302
|
|
|
1129
1303
|
spawnConfigs.forEach((config) => {
|
|
1130
1304
|
const { sel, typewriter: isTypewriter, typewriterSplit } = config
|
|
1131
|
-
|
|
1132
|
-
|
|
1305
|
+
qAllAllVariants(sel).forEach((el) => {
|
|
1306
|
+
const run = () => {
|
|
1307
|
+
if (hasGClass(el, "scroll") || hasGClass(el, "scroll-progress")) return
|
|
1133
1308
|
if (isPreserved(el)) return
|
|
1134
1309
|
if (isReduced(el)) return
|
|
1310
|
+
// if this sel is gated via bp:sel and not active, elementMatchesSel will have already filtered,
|
|
1311
|
+
// but qAllAllVariants includes inactive; check active before building
|
|
1312
|
+
if (!elementMatchesSel(el, sel)) return
|
|
1135
1313
|
const { delay, duration } = readTiming(el)
|
|
1136
1314
|
if (isTypewriter) {
|
|
1137
|
-
const easeClass = [...el.classList].find(c => c.startsWith("ease-"))
|
|
1138
|
-
const elEase = easeClass ? easeClass.
|
|
1315
|
+
const easeClass = getActivePrefixedClass(el, "ease-") || [...el.classList].find(c => c.startsWith("ease-"))
|
|
1316
|
+
const elEase = easeClass ? easeClass.slice(easeClass.indexOf("ease-") + 5) : "none"
|
|
1139
1317
|
if (typewriterSplit) {
|
|
1140
1318
|
el._spawnTween = playTypewriterSplit(el, delay, duration, elEase)
|
|
1141
1319
|
} else {
|
|
@@ -1145,8 +1323,8 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1145
1323
|
} else {
|
|
1146
1324
|
el._spawnTween = invokePlay(config, el, delay, duration, getEase(el))
|
|
1147
1325
|
el._spawnTween.eventCallback("onComplete", () => {
|
|
1148
|
-
if (el
|
|
1149
|
-
if (el
|
|
1326
|
+
if (hasGClass(el, "leave")) refreshLeaveRect(el)
|
|
1327
|
+
if (hasGClass(el, "flip")) captureFlip(el)
|
|
1150
1328
|
if (isCompatibility(el)) resumeCompatLoops(el)
|
|
1151
1329
|
scheduleRefresh()
|
|
1152
1330
|
fireOnComplete(el, "spawn")
|
|
@@ -1154,6 +1332,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1154
1332
|
}
|
|
1155
1333
|
markPreserved(el)
|
|
1156
1334
|
el.dataset.gsapWired = "1"
|
|
1335
|
+
}; runWithBreakpointForSel(el, sel, run)
|
|
1157
1336
|
})
|
|
1158
1337
|
})
|
|
1159
1338
|
|
|
@@ -1163,14 +1342,17 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1163
1342
|
spawnConfigs.forEach(({ sel, from, typewriter: isTypewriter, text }) => {
|
|
1164
1343
|
if (isTypewriter || text === false) return
|
|
1165
1344
|
const tSel = "." + TEXT_PREFIX + sel.slice(1)
|
|
1166
|
-
|
|
1167
|
-
|
|
1345
|
+
qAllAllVariants(tSel).forEach((el) => {
|
|
1346
|
+
const run = () => {
|
|
1347
|
+
if (hasGClass(el, "scroll") || hasGClass(el, "scroll-progress")) return
|
|
1168
1348
|
if (isPreserved(el)) return
|
|
1169
1349
|
if (isReduced(el)) return
|
|
1350
|
+
if (!elementMatchesSel(el, tSel)) return
|
|
1170
1351
|
const { delay, duration } = readTiming(el)
|
|
1171
1352
|
el._spawnTween = playText(el, from, delay, duration, getEase(el))
|
|
1172
1353
|
markPreserved(el)
|
|
1173
1354
|
el.dataset.gsapWired = "1"
|
|
1355
|
+
}; runWithBreakpointForSel(el, tSel, run)
|
|
1174
1356
|
})
|
|
1175
1357
|
})
|
|
1176
1358
|
|
|
@@ -1218,8 +1400,8 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1218
1400
|
}
|
|
1219
1401
|
|
|
1220
1402
|
const setupMagnet = (el) => {
|
|
1221
|
-
if (!el
|
|
1222
|
-
const threeD = el
|
|
1403
|
+
if (!hasGClass(el, "magnet") && !hasGClass(el, "magnet3d")) return
|
|
1404
|
+
const threeD = hasGClass(el, "magnet3d")
|
|
1223
1405
|
const duration = readClassNumber(el, "mtime-", 0.4)
|
|
1224
1406
|
const pull = readClassNumber(el, "amount-", 0.3)
|
|
1225
1407
|
const grow = readClassNumber(el, "mgrow-", 1.1)
|
|
@@ -1244,7 +1426,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1244
1426
|
// a hover/click is active we pause every tracked loop tween on the element
|
|
1245
1427
|
// and resume it once the interaction ends. Loops are only tracked when the
|
|
1246
1428
|
// `.compatibility` class is present, so nothing else changes behaviour.
|
|
1247
|
-
const isCompatibility = (el) => el
|
|
1429
|
+
const isCompatibility = (el) => hasGClass(el, "compatibility")
|
|
1248
1430
|
const compatLoopsOf = (el) => {
|
|
1249
1431
|
if (!el._gsapCompatLoops) el._gsapCompatLoops = []
|
|
1250
1432
|
return el._gsapCompatLoops
|
|
@@ -1259,7 +1441,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1259
1441
|
const setupClicks = (el) => {
|
|
1260
1442
|
if (isReduced(el)) return
|
|
1261
1443
|
setupMagnet(el)
|
|
1262
|
-
if (el
|
|
1444
|
+
if (hasGClass(el, "click-hover")) {
|
|
1263
1445
|
const area = wrapTarget(el)
|
|
1264
1446
|
let touch = false
|
|
1265
1447
|
const duration = readClassNumber(el, "ctime-", defaults.clickDuration)
|
|
@@ -1276,7 +1458,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1276
1458
|
touch = true, verticalmove(el, 0, duration, elEase), setTimeout(() => { touch = false }, 0)
|
|
1277
1459
|
})
|
|
1278
1460
|
}
|
|
1279
|
-
if (el
|
|
1461
|
+
if (hasGClass(el, "click-expand")) {
|
|
1280
1462
|
let touch = false
|
|
1281
1463
|
const duration = readClassNumber(el, "ctime-", defaults.clickDuration)
|
|
1282
1464
|
const lift = readClassNumber(el, "amount-", defaults.clickExpandOffset)
|
|
@@ -1313,7 +1495,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1313
1495
|
if (isReduced(el)) return
|
|
1314
1496
|
const ctx = readLoopCtx(el)
|
|
1315
1497
|
loopConfigs.forEach(({ sel, build, key, loop }) => {
|
|
1316
|
-
if (el
|
|
1498
|
+
if (elementMatchesSel(el, sel)) {
|
|
1317
1499
|
el[key]?.kill()
|
|
1318
1500
|
el[key] = trackCompatLoop(el, build(el, ctx))
|
|
1319
1501
|
if (loop) el[key].repeat(-1)
|
|
@@ -1368,7 +1550,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1368
1550
|
// shifts under the cursor. Marquee is skipped (its build restructures the
|
|
1369
1551
|
// DOM).
|
|
1370
1552
|
const wrapTarget = (el) => {
|
|
1371
|
-
if (!el
|
|
1553
|
+
if (!hasGClass(el, "wrapdiv")) return el
|
|
1372
1554
|
if (el._gsapWrap) return el._gsapWrap
|
|
1373
1555
|
const area = document.createElement("div")
|
|
1374
1556
|
el.before(area)
|
|
@@ -1383,7 +1565,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1383
1565
|
loopConfigs.forEach(({ sel, build, key }) => {
|
|
1384
1566
|
if (sel.startsWith(".marquee")) return
|
|
1385
1567
|
const name = sel.slice(1)
|
|
1386
|
-
if (el
|
|
1568
|
+
if (hasGClass(el, "hover-" + name)) {
|
|
1387
1569
|
const area = wrapTarget(el)
|
|
1388
1570
|
addListener(area, "mouseenter", () => {
|
|
1389
1571
|
pauseCompatLoops(el)
|
|
@@ -1396,7 +1578,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1396
1578
|
el[key] = reset(el, readClassNumber(el, "etime-", defaults.effectDuration), getEase(el))
|
|
1397
1579
|
resumeCompatLoops(el)
|
|
1398
1580
|
})
|
|
1399
|
-
} else if (el
|
|
1581
|
+
} else if (hasGClass(el, "click-" + name)) {
|
|
1400
1582
|
const area = wrapTarget(el)
|
|
1401
1583
|
addListener(area, "mousedown", () => {
|
|
1402
1584
|
pauseCompatLoops(el)
|
|
@@ -1519,7 +1701,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1519
1701
|
if (isReduced(el)) return
|
|
1520
1702
|
const ctx = readLoopCtx(el)
|
|
1521
1703
|
animAll.forEach((a) => {
|
|
1522
|
-
if (a.setup && el
|
|
1704
|
+
if (a.setup && elementMatchesSel(el, a.sel)) {
|
|
1523
1705
|
const teardown = a.setup(el, ctx)
|
|
1524
1706
|
if (typeof teardown === "function") setupTeardowns.push(teardown)
|
|
1525
1707
|
}
|
|
@@ -1571,13 +1753,13 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1571
1753
|
|
|
1572
1754
|
|
|
1573
1755
|
const animateAppear = (el) => {
|
|
1574
|
-
if (!el
|
|
1756
|
+
if (!hasGClass(el, "appear") || el._appeared) return
|
|
1575
1757
|
// A `.scroll`/`.scroll-progress` element is owned by its ScrollTrigger
|
|
1576
1758
|
// (see setupScroll); `.appear` must not also fire, or it plays on mount
|
|
1577
1759
|
// AND again on scroll-enter. Text elements are the exception: their
|
|
1578
1760
|
// `.scroll` triggers are wired once at init, so a re-added (reset) text
|
|
1579
1761
|
// element has no trigger to conflict with and must animate via `.appear`.
|
|
1580
|
-
if (!isTextElement(el) && (el
|
|
1762
|
+
if (!isTextElement(el) && (hasGClass(el, "scroll") || hasGClass(el, "scroll-progress"))) return
|
|
1581
1763
|
if (isReduced(el)) return
|
|
1582
1764
|
el._appeared = true
|
|
1583
1765
|
const { delay, duration, ease } = readTiming(el)
|
|
@@ -1598,8 +1780,8 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1598
1780
|
} else {
|
|
1599
1781
|
el._spawnTween = invokePlay(config, el, delay, duration, ease)
|
|
1600
1782
|
el._spawnTween.eventCallback("onComplete", () => {
|
|
1601
|
-
if (el
|
|
1602
|
-
if (el
|
|
1783
|
+
if (hasGClass(el, "leave")) refreshLeaveRect(el)
|
|
1784
|
+
if (hasGClass(el, "flip")) captureFlip(el)
|
|
1603
1785
|
fireOnComplete(el, "spawn")
|
|
1604
1786
|
})
|
|
1605
1787
|
}
|
|
@@ -1630,7 +1812,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1630
1812
|
els.forEach((el) => {
|
|
1631
1813
|
// `.appear` is the opt-in gate for dynamically-added
|
|
1632
1814
|
// elements: without it a newly inserted node is ignored.
|
|
1633
|
-
if (!el
|
|
1815
|
+
if (!hasGClass(el, "appear")) return
|
|
1634
1816
|
animateAppear(el)
|
|
1635
1817
|
setupScroll(el)
|
|
1636
1818
|
setupScrollDriven(el)
|
|
@@ -1710,7 +1892,7 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1710
1892
|
appearObserver.observe(document.body, { childList: true, subtree: true })
|
|
1711
1893
|
|
|
1712
1894
|
// Capture any .leave elements already present so they can exit later
|
|
1713
|
-
qAll(".leave").forEach(captureLeave)
|
|
1895
|
+
qAll("body *").filter(el => hasGClass(el, "leave")).forEach(captureLeave)
|
|
1714
1896
|
|
|
1715
1897
|
leaveObserver = new MutationObserver((mutations) => {
|
|
1716
1898
|
handleLeaveBatch(mutations)
|
|
@@ -1720,16 +1902,17 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1720
1902
|
}
|
|
1721
1903
|
if (throttlePerFrame > 0) {
|
|
1722
1904
|
// capture for throttled path (was inside else branch above for non-throttled)
|
|
1723
|
-
qAll(".leave").forEach(captureLeave)
|
|
1905
|
+
qAll("body *").filter(el => hasGClass(el, "leave")).forEach(captureLeave)
|
|
1724
1906
|
}
|
|
1725
1907
|
|
|
1726
|
-
// Keep the captured position fresh (throttled to one pass per frame)
|
|
1908
|
+
// Keep the captured position fresh (throttled to one pass per frame) - include bp:leave
|
|
1909
|
+
const qAllLeaves = () => qAll("body *").filter(el => hasGClass(el, "leave"))
|
|
1727
1910
|
let positionTick = false
|
|
1728
1911
|
const refreshLeavePositions = () => {
|
|
1729
1912
|
if (positionTick) return
|
|
1730
1913
|
positionTick = true
|
|
1731
1914
|
requestAnimationFrame(() => {
|
|
1732
|
-
|
|
1915
|
+
qAllLeaves().forEach((el) => {
|
|
1733
1916
|
const s = leaveStates.get(el)
|
|
1734
1917
|
if (s) s.rect = el.getBoundingClientRect()
|
|
1735
1918
|
})
|
|
@@ -1738,6 +1921,18 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1738
1921
|
}
|
|
1739
1922
|
window.addEventListener("scroll", refreshLeavePositions, { passive: true })
|
|
1740
1923
|
window.addEventListener("resize", refreshLeavePositions, { passive: true })
|
|
1924
|
+
// Breakpoint reactivity: when crossing a breakpoint, modifiers like m:amount-20 or m:time-2
|
|
1925
|
+
// need to be re-evaluated (GSAP tweens built with old values are stale). Each breakpoint
|
|
1926
|
+
// matchMedia entry is naturally handled by runWithBreakpoint/mm.add for gated animations,
|
|
1927
|
+
// but modifier-only changes (e.g. `amount-10 m:amount-30` on same element) require a rebuild.
|
|
1928
|
+
// Listen to all breakpoints and refresh ScrollTrigger + re-evaluate active prefixed classes.
|
|
1929
|
+
const bpMqls = bpEntries.map(([bp, px]) => {
|
|
1930
|
+
const mql = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia(`(min-width: ${px}px)`) : null
|
|
1931
|
+
if (!mql) return null
|
|
1932
|
+
const fn = () => ScrollTrigger.refresh()
|
|
1933
|
+
mql.addEventListener?.('change', fn)
|
|
1934
|
+
return { mql, fn }
|
|
1935
|
+
}).filter(Boolean)
|
|
1741
1936
|
|
|
1742
1937
|
return () => {
|
|
1743
1938
|
appearObserver?.disconnect()
|
|
@@ -1748,6 +1943,8 @@ export default function initListeners(root = document, throttlePerFrame) {
|
|
|
1748
1943
|
if (flipPendingRaf) cancelAnimationFrame(flipPendingRaf)
|
|
1749
1944
|
window.removeEventListener("scroll", refreshLeavePositions)
|
|
1750
1945
|
window.removeEventListener("resize", refreshLeavePositions)
|
|
1946
|
+
bpMqls.forEach(({ mql, fn }) => mql.removeEventListener?.('change', fn))
|
|
1947
|
+
mm.revert()
|
|
1751
1948
|
window.removeEventListener("load", ScrollTrigger.refresh)
|
|
1752
1949
|
clearTimeout(refreshTimer)
|
|
1753
1950
|
scrollTriggers.forEach((t) => {
|