gclass-anims 1.0.0-beta.7 → 1.0.0-beta.8
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/Animations.js +54 -23
- package/Listeners.js +52 -12
- package/package.json +1 -1
package/Animations.js
CHANGED
|
@@ -340,6 +340,9 @@ export function pulse (delay , target , amount , dur , ease){
|
|
|
340
340
|
|
|
341
341
|
export function radiate (delay , target , amount , dur , ease , zIndex){
|
|
342
342
|
const clone = target.cloneNode(true)
|
|
343
|
+
// Tagged so engine teardown can sweep up clones whose tween was killed
|
|
344
|
+
// before its onComplete (route changes mid-animation).
|
|
345
|
+
clone.setAttribute("data-gsap-radiate", "1")
|
|
343
346
|
clone.style.cssText = `position:fixed;left:0;top:0;right:auto;bottom:auto;margin:0;pointer-events:none;transform-origin:50% 50%;${zIndex != null ? `z-index:${zIndex};` : ""}`
|
|
344
347
|
// Keep the ripple glued to the target so it tracks scroll/resize instead of
|
|
345
348
|
// getting stranded at the position captured when the animation was built.
|
|
@@ -363,6 +366,13 @@ export function radiate (delay , target , amount , dur , ease , zIndex){
|
|
|
363
366
|
applyRect()
|
|
364
367
|
window.addEventListener("scroll", schedule, { passive: true })
|
|
365
368
|
window.addEventListener("resize", schedule, { passive: true })
|
|
369
|
+
// Killing the tween (teardown, hover/click rebuilds) must clean up exactly
|
|
370
|
+
// like natural completion — otherwise clones + listeners leak.
|
|
371
|
+
const cleanup = () => {
|
|
372
|
+
clone.remove()
|
|
373
|
+
window.removeEventListener("scroll", schedule)
|
|
374
|
+
window.removeEventListener("resize", schedule)
|
|
375
|
+
}
|
|
366
376
|
|
|
367
377
|
return gsap.fromTo(clone , {scale:1 , opacity:1} , {
|
|
368
378
|
scale:amount / 10 ,
|
|
@@ -370,11 +380,8 @@ export function radiate (delay , target , amount , dur , ease , zIndex){
|
|
|
370
380
|
duration:dur ,
|
|
371
381
|
delay:delay ,
|
|
372
382
|
ease:easeOf(ease) ,
|
|
373
|
-
onComplete:
|
|
374
|
-
|
|
375
|
-
window.removeEventListener("scroll", schedule)
|
|
376
|
-
window.removeEventListener("resize", schedule)
|
|
377
|
-
} ,
|
|
383
|
+
onComplete: cleanup ,
|
|
384
|
+
onInterrupt: cleanup ,
|
|
378
385
|
})
|
|
379
386
|
}
|
|
380
387
|
|
|
@@ -400,28 +407,52 @@ export function marquee (target , dir , duration , xOffset = 0 , yOffset = 0 , n
|
|
|
400
407
|
// which opens a gap on the trailing edge at some point in the loop.
|
|
401
408
|
target.style.position = "relative"
|
|
402
409
|
target.style.overflow = "hidden"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
|
|
411
|
+
// Rebuilds over the SAME element (engine restarts, StrictMode remounts)
|
|
412
|
+
// must reuse the existing track. Re-creating it would swallow the old
|
|
413
|
+
// absolute track as the first "child", repeat THAT as the tiling unit —
|
|
414
|
+
// every copy stacks at the same offset and scrollWidth collapses.
|
|
415
|
+
let track = target._gcTrack
|
|
416
|
+
if (!track || !track.isConnected) {
|
|
417
|
+
track = document.createElement("div")
|
|
418
|
+
track.style.cssText = `position:absolute;top:${yOffset}px;left:${xOffset}px;display:flex;flex-direction:${horizontal ? "row" : "column"};width:max-content;will-change:transform;`
|
|
419
|
+
while (target.firstChild) track.appendChild(target.firstChild)
|
|
420
|
+
target.appendChild(track)
|
|
421
|
+
// Guard against legacy poisoned DOM: older builds could wrap a previous
|
|
422
|
+
// absolute track inside this one. Unwrap any nested engine tracks so
|
|
423
|
+
// the stashed unit is always the raw content.
|
|
424
|
+
while (
|
|
425
|
+
track.children.length &&
|
|
426
|
+
[...track.children].every((c) => c.style.position === "absolute" && c.style.display === "flex")
|
|
427
|
+
) {
|
|
428
|
+
const inner = track.firstElementChild
|
|
429
|
+
while (inner.firstChild) track.insertBefore(inner.firstChild, inner)
|
|
430
|
+
track.removeChild(inner)
|
|
431
|
+
}
|
|
432
|
+
target._gcUnitHtml = track.innerHTML
|
|
433
|
+
}
|
|
434
|
+
track.style.flexDirection = horizontal ? "row" : "column"
|
|
435
|
+
const unitHtml = target._gcUnitHtml
|
|
436
|
+
|
|
437
|
+
// Measure ONE unit on its own: the live track may already hold N copies
|
|
438
|
+
// (or stale content), which would inflate unitSize and starve `copies`.
|
|
439
|
+
const measurer = document.createElement("div")
|
|
440
|
+
measurer.style.cssText = track.style.cssText + "visibility:hidden;"
|
|
441
|
+
measurer.innerHTML = unitHtml
|
|
442
|
+
target.appendChild(measurer)
|
|
443
|
+
const unitSize = horizontal ? measurer.scrollWidth : measurer.scrollHeight
|
|
444
|
+
measurer.remove()
|
|
445
|
+
|
|
446
|
+
// Nothing to tile (empty content) — return an inert tween rather than one
|
|
447
|
+
// dividing by a zero-width unit.
|
|
448
|
+
if (!unitSize) return gsap.fromTo(track, {}, { duration: 0 })
|
|
449
|
+
|
|
450
|
+
// The track is absolutely positioned, so a bare-text host can collapse to
|
|
451
|
+
// zero height and `overflow:hidden` would clip the strip away entirely.
|
|
414
452
|
if (target.offsetHeight === 0 && track.offsetHeight > 0) {
|
|
415
453
|
target.style.height = track.offsetHeight + "px"
|
|
416
454
|
}
|
|
417
455
|
|
|
418
|
-
const first = track.children[0]
|
|
419
|
-
if (!first) return gsap.fromTo(track, {}, { duration: 0 })
|
|
420
|
-
|
|
421
|
-
// A single copy of the content (the width one loop step must travel).
|
|
422
|
-
const unitHtml = track.innerHTML
|
|
423
|
-
const unitSize = horizontal ? track.scrollWidth : track.scrollHeight
|
|
424
|
-
|
|
425
456
|
// Default: repeat the unit until the whole strip is at least as wide as the
|
|
426
457
|
// viewport (plus one extra copy so the trailing edge stays covered mid-loop).
|
|
427
458
|
// `.marquee-no-repeat` opts into the minimal 2-copy single-seam behaviour.
|
package/Listeners.js
CHANGED
|
@@ -57,19 +57,37 @@ export default function initListeners() {
|
|
|
57
57
|
|
|
58
58
|
// `.preserve` keeps an already-rendered element (e.g. one that persists
|
|
59
59
|
// in a shared layout across route changes) from being re-animated when
|
|
60
|
-
// the Listeners setup re-runs.
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
60
|
+
// the Listeners setup re-runs. It applies to the element AND its
|
|
61
|
+
// children: any preserved ancestor also suppresses animation on this
|
|
62
|
+
// node. Two subtleties make this behave correctly:
|
|
63
|
+
// • markPreserved() tags the FULL preserve-ancestor chain, so a bare
|
|
64
|
+
// `.preserve` container without its own spawn class (a site header,
|
|
65
|
+
// say) still gets tagged when one of its children animates.
|
|
66
|
+
// • Suppression requires the element ITSELF to carry data-gsap-wired
|
|
67
|
+
// (set when a previous run animated it). Freshly mounted content
|
|
68
|
+
// under a preserved root therefore still plays its entrance — only
|
|
69
|
+
// DOM that survived from an earlier run stays frozen.
|
|
66
70
|
const isPreserved = (el) => {
|
|
71
|
+
if (!el.dataset.gsapWired) return false
|
|
72
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
73
|
+
if (node.classList.contains("preserve") && node.dataset.gsapPreserved) return true
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
const markPreserved = (el) => {
|
|
78
|
+
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
79
|
+
if (node.classList.contains("preserve")) node.dataset.gsapPreserved = "1"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// True when el sits inside a `.preserve` root that a run has already
|
|
83
|
+
// tagged. Such regions are frozen by design: later runs skip them, so
|
|
84
|
+
// teardown must leave their visual state untouched too.
|
|
85
|
+
const underPreservedRoot = (el) => {
|
|
67
86
|
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
68
87
|
if (node.classList.contains("preserve") && node.dataset.gsapPreserved) return true
|
|
69
88
|
}
|
|
70
89
|
return false
|
|
71
90
|
}
|
|
72
|
-
const markPreserved = (el) => { if (el.classList.contains("preserve")) el.dataset.gsapPreserved = "1" }
|
|
73
91
|
|
|
74
92
|
// `spawnConfigs` is derived from the config in Config.js (see top of
|
|
75
93
|
// file). Adding/removing an entry there automatically re-wires every
|
|
@@ -699,7 +717,7 @@ export default function initListeners() {
|
|
|
699
717
|
scrollTriggers.push(t.scrollTrigger)
|
|
700
718
|
}
|
|
701
719
|
}
|
|
702
|
-
gsap.utils.toArray('[class^="parallax-"], .progress-bar, .scroll-fill, .scroll-fade-bg, .scroll-horizontal').forEach(setupScrollDriven)
|
|
720
|
+
gsap.utils.toArray('[class^="parallax-"],[class*=" parallax-"], .progress-bar, .scroll-fill, .scroll-fade-bg, .scroll-horizontal').forEach(setupScrollDriven)
|
|
703
721
|
|
|
704
722
|
// `.scroll`/`.scroll-progress` entrance animation, driven by ScrollTrigger.
|
|
705
723
|
// Split out into a helper so DYNAMICALLY-added elements (e.g. pagination
|
|
@@ -915,6 +933,7 @@ export default function initListeners() {
|
|
|
915
933
|
})
|
|
916
934
|
}
|
|
917
935
|
markPreserved(el)
|
|
936
|
+
el.dataset.gsapWired = "1"
|
|
918
937
|
})
|
|
919
938
|
})
|
|
920
939
|
|
|
@@ -931,6 +950,7 @@ export default function initListeners() {
|
|
|
931
950
|
const { delay, duration } = readTiming(el)
|
|
932
951
|
el._spawnTween = playText(el, from, delay, duration, getEase(el))
|
|
933
952
|
markPreserved(el)
|
|
953
|
+
el.dataset.gsapWired = "1"
|
|
934
954
|
})
|
|
935
955
|
})
|
|
936
956
|
|
|
@@ -1077,8 +1097,11 @@ export default function initListeners() {
|
|
|
1077
1097
|
el[key]?.kill()
|
|
1078
1098
|
el[key] = trackCompatLoop(el, build(el, ctx))
|
|
1079
1099
|
if (loop) el[key].repeat(-1)
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1100
|
+
// Track wired loops so teardown can kill them (radiate
|
|
1101
|
+
// relies on kill/onInterrupt to remove its clones).
|
|
1102
|
+
if (!loopEls.some((l) => l.el === el && l.key === key)) {
|
|
1103
|
+
loopEls.push({ el, key })
|
|
1104
|
+
}
|
|
1082
1105
|
el[key].eventCallback(el[key].repeat() === -1 ? "onRepeat" : "onComplete",
|
|
1083
1106
|
() => fireOnComplete(el, "loop"))
|
|
1084
1107
|
}
|
|
@@ -1474,13 +1497,30 @@ export default function initListeners() {
|
|
|
1474
1497
|
magnetListeners.forEach(({ el, type, fn }) => el.removeEventListener(type, fn))
|
|
1475
1498
|
magnetQuery?.removeEventListener("change", applyMagnet)
|
|
1476
1499
|
loopEls.forEach(({ el, key }) => el[key]?.kill())
|
|
1500
|
+
// Sweep any radiate clones orphaned by pre-fix kills or edge cases.
|
|
1501
|
+
document.querySelectorAll('[data-gsap-radiate]').forEach((n) => n.remove())
|
|
1477
1502
|
cssTweens.forEach((t) => t?.kill())
|
|
1478
1503
|
cssTweens.length = 0
|
|
1479
1504
|
gsap.utils.toArray(".typewriter").forEach(el => {
|
|
1480
|
-
|
|
1505
|
+
// Preserved-region typewriters stay as-is (already at their end
|
|
1506
|
+
// state); finalize the rest so a mid-type kill can't strand
|
|
1507
|
+
// partial text where the next run's stash would read it.
|
|
1508
|
+
const keep = el.isConnected && underPreservedRoot(el)
|
|
1509
|
+
if (!keep && el.typewriter && !el.typewriter.reversed()) el.typewriter.progress(1)
|
|
1481
1510
|
el.typewriter?.kill()
|
|
1482
1511
|
})
|
|
1483
|
-
textSplits.forEach((s) =>
|
|
1512
|
+
textSplits.forEach((s) => {
|
|
1513
|
+
// Splits inside a tagged preserve region keep their spans — the
|
|
1514
|
+
// next run will skip those elements, and reverting here would
|
|
1515
|
+
// visibly strip their finished animation. Everything else reverts
|
|
1516
|
+
// cleanly (and drops out of splitCache so a reused element can be
|
|
1517
|
+
// re-split fresh).
|
|
1518
|
+
const keep = (s.elements || []).some((e) => e.isConnected && underPreservedRoot(e))
|
|
1519
|
+
if (!keep) {
|
|
1520
|
+
;(s.elements || []).forEach((e) => splitCache.delete(e))
|
|
1521
|
+
s.revert()
|
|
1522
|
+
}
|
|
1523
|
+
})
|
|
1484
1524
|
textSplits.length = 0
|
|
1485
1525
|
onCompleteTweens.forEach((t) => t?.kill())
|
|
1486
1526
|
onCompleteTweens.length = 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gclass-anims",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.8",
|
|
4
4
|
"description": "A Tailwind-style utility layer on top of GSAP. Framework-agnostic — works in vanilla JS, React, Vue, Svelte, or any bundler.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|