gclass-anims 1.0.0-beta.14 → 1.0.0-beta.16

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 CHANGED
@@ -1,10 +1,12 @@
1
1
  import initListeners from './Listeners'
2
+ import { defaults, animations } from './Config.js'
3
+ import gsap from 'gsap'
2
4
 
3
5
  // localStorage key controlling whether the GSAP animation system is mounted.
4
- const STORAGE_KEY = 'funbyte-animations-enabled'
6
+ const STORAGE_KEY = 'gclass-animations-enabled'
5
7
  // localStorage key for a forced reduced-motion override (see
6
8
  // enableReducedMotion / disableReducedMotion).
7
- const REDUCED_KEY = 'funbyte-reduced-motion'
9
+ const REDUCED_KEY = 'gclass-reduced-motion'
8
10
 
9
11
  const reducedMotionQuery = typeof window !== 'undefined'
10
12
  ? window.matchMedia('(prefers-reduced-motion: reduce)')
@@ -111,12 +113,106 @@ export function disableReducedMotion() {
111
113
  }
112
114
 
113
115
  let cleanup = null
116
+ let bootCleanup = null
117
+ let bootTimeout = null
118
+ let bootStyle = null
119
+
120
+ const readBootTime = (els, fallback) => {
121
+ let max = null
122
+ for (const el of els) {
123
+ const cls = [...el.classList].find(c => c.startsWith('boot-time-'))
124
+ if (cls) {
125
+ const n = Number(cls.slice('boot-time-'.length))
126
+ if (!Number.isNaN(n)) max = max === null ? n : Math.max(max, n)
127
+ }
128
+ }
129
+ return max ?? fallback
130
+ }
114
131
 
115
132
  // Boots the GSAP animation system unless animations are disabled (stored "off"
116
133
  // or reduced-motion fallback with no explicit choice). Idempotent: calling it
117
134
  // again tears down any previous run first.
135
+ // Now also handles boot screen: any HTML/JSX with `.boot-up` anywhere is treated as the boot overlay.
136
+ // No separate initBoot needed - just call initAnimations().
137
+ // Boot stops all DOM rendering for defaults.bootTime (overwritten by boot-time-N class).
118
138
  export function initAnimations() {
119
139
  if (typeof window === 'undefined' || !getEnabled()) return
120
- if (cleanup) cleanup()
140
+ if (cleanup) { cleanup(); cleanup = null }
141
+ if (bootCleanup) { bootCleanup(); bootCleanup = null }
142
+ if (bootTimeout) { clearTimeout(bootTimeout); bootTimeout = null }
143
+ if (bootStyle) { bootStyle.remove(); bootStyle = null; document.documentElement.classList.remove('gclass-booting') }
144
+
145
+ const bootEls = typeof document !== 'undefined' ? document.querySelectorAll(".boot-up") : []
146
+ if (!bootEls.length) {
147
+ // no .boot-up -> completely skip boot
148
+ } else if (bootEls.length > 1) {
149
+ console.error(`[initAnimations] Multiple .boot-up elements detected (${bootEls.length}) - skipping all boot animations`, bootEls)
150
+ bootEls.forEach(el => el.remove())
151
+ // fall through to normal initListeners without pausing DOM
152
+ } else {
153
+ const bootEl = bootEls[0]
154
+ const hasBootEnd = [...bootEl.classList].some(c => c.startsWith('boot-end-'))
155
+ if (!hasBootEnd) {
156
+ // no boot-end-* -> completely skip boot animation (still pause? spec says skip it)
157
+ // spec: skip boot-end animation if no class, but still do boot pause? user said "completely skip it if no .boot-end-<name> class is present"
158
+ // interpret as skip the exit animation only, still pause for bootTime
159
+ // To match "completely skip it" for boot-end, we just don't play exit tween
160
+ }
161
+ const bootTime = readBootTime(bootEls, defaults.bootTime ?? 2)
162
+ console.log(`[initAnimations] .boot-up found: ${bootEls.length} - pausing DOM for ${bootTime}s`, bootEls)
163
+ // stop all DOM rendering except .boot-up
164
+ bootStyle = document.createElement('style')
165
+ bootStyle.id = 'gclass-boot-style'
166
+ bootStyle.textContent = `html.gclass-booting{visibility:hidden} html.gclass-booting .boot-up,html.gclass-booting .boot-up *{visibility:visible} html.gclass-booting .boot-up{position:fixed;inset:0;z-index:9999;display:grid;place-items:center}`
167
+ document.head.appendChild(bootStyle)
168
+ document.documentElement.classList.add('gclass-booting')
169
+ // ensure boot els are visible even if nested inside hidden ancestors
170
+ bootEls.forEach(el => { el.style.visibility = 'visible' })
171
+
172
+ // animations inside boot screen must play while rest of DOM is hidden - init scoped to boot-up
173
+ bootCleanup = initListeners(bootEl)
174
+
175
+ bootTimeout = setTimeout(() => {
176
+ const bootEndCls = [...bootEl.classList].find(c => c.startsWith('boot-end-'))
177
+ if (!bootEndCls) {
178
+ // no boot-end -> skip exit animation, just remove
179
+ bootCleanup?.(); bootCleanup = null
180
+ document.documentElement.classList.remove('gclass-booting')
181
+ bootStyle?.remove(); bootStyle = null
182
+ bootEls.forEach(el => el.remove())
183
+ bootTimeout = null
184
+ cleanup = initListeners()
185
+ return
186
+ }
187
+ const name = bootEndCls.slice('boot-end-'.length) // e.g. spawn-blur
188
+ const cfg = animations.find(a => a.sel === '.' + name)
189
+ const from = cfg?.from
190
+ const easeCl = [...bootEl.classList].find(c => c.startsWith('ease-'))
191
+ const ease = easeCl ? easeCl.split('-')[1] : defaults.ease
192
+ const dur = readBootTime([bootEl], defaults.effectDuration ?? 1) // reuse boot-time- or fallback to effectDuration; if boot-time used for pause, reuse same value for exit unless overridden
193
+ // Actually use boot-end-time-N if present, else effectDuration
194
+ const endTimeCls = [...bootEl.classList].find(c => c.startsWith('boot-end-time-'))
195
+ const endDur = endTimeCls ? Number(endTimeCls.slice('boot-end-time-'.length)) : dur
196
+
197
+ const finish = () => {
198
+ bootCleanup?.(); bootCleanup = null
199
+ document.documentElement.classList.remove('gclass-booting')
200
+ bootStyle?.remove(); bootStyle = null
201
+ bootEls.forEach(el => el.remove())
202
+ bootTimeout = null
203
+ cleanup = initListeners()
204
+ }
205
+
206
+ if (!cfg || !from) {
207
+ console.warn(`[initAnimations] boot-end-${name} has no from state - removing without animation`)
208
+ finish()
209
+ return
210
+ }
211
+ // play spawn in reverse (visible -> hidden) before removing
212
+ gsap.to(bootEl, { ...from, duration: endDur, ease, onComplete: finish })
213
+ }, bootTime * 1000)
214
+ return
215
+ }
216
+
121
217
  cleanup = initListeners()
122
218
  }
package/Animations.js CHANGED
@@ -79,7 +79,11 @@ export function expandA (target , delay , dur , ease){
79
79
  }
80
80
 
81
81
  export function typewriter (target , text , dur , delay , ease){
82
- return gsap.fromTo(target , {text:""} , {ease:easeOf(ease) , duration:dur , delay:delay , text:text})
82
+ // Any element using TextPlugin gets pre-wrap so "\n" from VSCode Enter or JS strings
83
+ // renders as a real line break and any innerHTML formatting is preserved.
84
+ gsap.utils.toArray(target).forEach(el => { if (el.style) el.style.whiteSpace = "pre-wrap" })
85
+ const value = String(text).replace(/\n/g, "<br>")
86
+ return gsap.fromTo(target , {text:""} , {ease:easeOf(ease) , duration:dur , delay:delay , text:{value}})
83
87
  }
84
88
 
85
89
  export function spawnSpinCCW (target , delay , dur , ease){
@@ -587,27 +591,43 @@ export function radiate (delay , target , amount , dur , ease , zIndex){
587
591
  tick = true
588
592
  requestAnimationFrame(() => { tick = false; applyRect() })
589
593
  }
594
+ // Don't animate detached targets — return inert tween
595
+ if (!target.isConnected) {
596
+ return gsap.fromTo(clone, {}, { duration: 0 })
597
+ }
590
598
  document.body.appendChild(clone)
591
599
  applyRect()
592
600
  window.addEventListener("scroll", schedule, { passive: true })
593
601
  window.addEventListener("resize", schedule, { passive: true })
594
602
  // Killing the tween (teardown, hover/click rebuilds) must clean up exactly
595
603
  // like natural completion - otherwise clones + listeners leak.
604
+ let tween = null
596
605
  const cleanup = () => {
597
606
  clone.remove()
598
607
  window.removeEventListener("scroll", schedule)
599
608
  window.removeEventListener("resize", schedule)
609
+ if (observer) observer.disconnect()
600
610
  }
611
+ // If target is removed from DOM (React unmount, .remove(), SPA navigation),
612
+ // kill the tween and remove the clone — mirrors React useEffect cleanup
613
+ const observer = new MutationObserver(() => {
614
+ if (!target.isConnected) {
615
+ if (tween) tween.kill()
616
+ cleanup()
617
+ }
618
+ })
619
+ observer.observe(document.body, { childList: true, subtree: true })
601
620
 
602
- return gsap.fromTo(clone , {scale:1 , opacity:1} , {
621
+ tween = gsap.fromTo(clone , {scale:1 , opacity:1} , {
603
622
  scale:amount / 10 ,
604
623
  opacity:0 ,
605
624
  duration:dur ,
606
625
  delay:delay ,
607
626
  ease:easeOf(ease) ,
608
- onComplete: cleanup ,
609
- onInterrupt: cleanup ,
627
+ onComplete: () => { observer.disconnect(); cleanup(); },
628
+ onInterrupt: () => { observer.disconnect(); cleanup(); },
610
629
  })
630
+ return tween
611
631
  }
612
632
 
613
633
 
package/Boot.js ADDED
@@ -0,0 +1,12 @@
1
+ import { initAnimations } from './AnimToggle.js'
2
+
3
+ export function Boot(){
4
+ const els = document.querySelectorAll(".boot-up")
5
+ console.log(`[Boot -> initAnimations] .boot-up count: ${els.length}`, els)
6
+ // rewired: Boot now just delegates to initAnimations (single entry point)
7
+ return initAnimations()
8
+ }
9
+ // keep named exports wired so index.js / dev don't break during test
10
+ export const initBoot = Boot
11
+ export const createBootOverlay = Boot
12
+ export default Boot
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to `gclass-anims` will be documented in this file.
4
4
 
5
+ ## [1.0.0-beta.16] - 2026-09-1
6
+ - Added a new `.boot-up` class for boot up animations
7
+ - Fixed text animations not taking formatting into account
8
+
9
+
5
10
  ## [1.0.0-beta.13] - 2026-08-27
6
11
  - Added a new `.fill-svg` modifier for the `.draw` and `.draw-split` classes that fills the SVG after it has been drawn.
7
12
 
package/Config.js CHANGED
@@ -70,7 +70,8 @@ export const defaults = {
70
70
  typewriterSplitCharDuration: 0.05,
71
71
  minTextPartDuration: 0.3,
72
72
  revealDelay:0,
73
- characterlist:"AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz"
73
+ characterlist:"AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz",
74
+ bootTime: 5,
74
75
  }
75
76
 
76
77
  export const animations = [
package/Listeners.js CHANGED
@@ -93,9 +93,21 @@ const invokePlay = (config, el, delay, dur, ease) => {
93
93
  }
94
94
  }
95
95
 
96
- export default function initListeners() {
96
+ export default function initListeners(root = document) {
97
97
  gsap.registerPlugin(TextPlugin, ScrollTrigger, SplitText)
98
98
 
99
+ // helper to scope queries to root (for boot screen: only boot-up subtree animates during boot)
100
+ const qAll = (sel) => {
101
+ if (root === document || root === document.documentElement || root === document.body) return gsap.utils.toArray(sel)
102
+ if (sel === "body *") return gsap.utils.toArray(root.querySelectorAll("*"))
103
+ try {
104
+ const els = [...(root.querySelectorAll ? root.querySelectorAll(sel) : [])]
105
+ if (root.matches?.(sel)) els.unshift(root)
106
+ // handle comma selectors where root itself may match one part
107
+ return els
108
+ } catch { return gsap.utils.toArray(sel) }
109
+ }
110
+
99
111
  const registeredListeners = []
100
112
  const onCompleteTweens = []
101
113
  const addListener = (el, type, fn) => {
@@ -358,7 +370,7 @@ export default function initListeners() {
358
370
  spawnConfigs.map(({ sel }) => "." + TEXT_PREFIX + sel.slice(1)).join(",")
359
371
 
360
372
  const getOrderDelay = (el, priority) => {
361
- const samepri = gsap.utils.toArray(orderSelector())
373
+ const samepri = qAll(orderSelector())
362
374
  .filter((e) => {
363
375
  if (!e.classList.contains("order")) return false
364
376
  const match = [...e.classList].find(p => p.startsWith("priority-"))
@@ -472,7 +484,10 @@ export default function initListeners() {
472
484
  // would render it in its connecting form instead of its correct END form),
473
485
  // but it should still be split into its own span so it animates too.
474
486
  const RTL_LETTER = /[\u0621-\u064A\u066E-\u06D5\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/
475
- const getRTLCharSplit = (el) => new SplitText(el, {
487
+ const getRTLCharSplit = (el) => {
488
+ // Any element using SplitText gets pre-wrap so "\n" and formatting is preserved
489
+ if (el.style) el.style.whiteSpace = "pre-wrap"
490
+ return new SplitText(el, {
476
491
  type: "words",
477
492
  linesClass: "gsap-line",
478
493
  wordsClass: "gsap-word",
@@ -555,6 +570,7 @@ export default function initListeners() {
555
570
  self.words.length = 0
556
571
  },
557
572
  })
573
+ }
558
574
 
559
575
  // Flex targets can't be split directly: the split parts would become
560
576
  // flex ITEMS, so justify-content/gap would apply per letter, whitespace-
@@ -595,6 +611,7 @@ export default function initListeners() {
595
611
  }
596
612
 
597
613
  const getSplit = (el, gran) => {
614
+ if (el.style) el.style.whiteSpace = "pre-wrap"
598
615
  let s = splitCache.get(el)
599
616
  const rtlChars = gran === "chars" && isRTLText(el)
600
617
  const key = rtlChars ? "rtl-chars" : gran
@@ -604,6 +621,9 @@ export default function initListeners() {
604
621
  ? getRTLCharSplit(el)
605
622
  : new SplitText(el, {
606
623
  type: gran,
624
+ tag: "span",
625
+ reduceWhiteSpace: false,
626
+
607
627
  linesClass: "gsap-line",
608
628
  wordsClass: "gsap-word",
609
629
  charsClass: "gsap-char",
@@ -739,7 +759,7 @@ export default function initListeners() {
739
759
  // created before any scroll/scroll-progress trigger measures its position.
740
760
  // Setting them up here (before the trigger pass below) keeps offsets correct
741
761
  // and lets the single ScrollTrigger.refresh() at the end reconcile layout.
742
- gsap.utils.toArray(".pin").forEach(setupPin)
762
+ qAll(".pin").forEach(setupPin)
743
763
 
744
764
  // Scroll-driven extras - class-driven ScrollTrigger behaviours that don't
745
765
  // fit the spawn/loop machinery (no `play`/`build`), handled like `.pin`:
@@ -832,7 +852,7 @@ export default function initListeners() {
832
852
  scrollTriggers.push(t.scrollTrigger)
833
853
  }
834
854
  }
835
- gsap.utils.toArray('[class^="parallax-"],[class*=" parallax-"], .progress-bar, .scroll-fill, .scroll-fade-bg, .scroll-horizontal').forEach(setupScrollDriven)
855
+ qAll('[class^="parallax-"],[class*=" parallax-"], .progress-bar, .scroll-fill, .scroll-fade-bg, .scroll-horizontal').forEach(setupScrollDriven)
836
856
 
837
857
  // Scroller resolution: a `.scroll`/`.scroll-progress` element inside a
838
858
  // `.scroll-frame` container binds its trigger to THAT box instead of the
@@ -1001,7 +1021,7 @@ export default function initListeners() {
1001
1021
  })
1002
1022
  scrollTriggers.push(st)
1003
1023
  }
1004
- gsap.utils.toArray(".scroll, .scroll-progress").forEach(setupScroll)
1024
+ qAll(".scroll, .scroll-progress").forEach(setupScroll)
1005
1025
 
1006
1026
  // SplitText scroll variants: `.spawn-text-<spawn>.scroll` plays the per-part
1007
1027
  // tween when the element enters the viewport and reverses on exit.
@@ -1009,7 +1029,7 @@ export default function initListeners() {
1009
1029
  if (isTypewriter || text === false) return
1010
1030
  const tSel = "." + TEXT_PREFIX + sel.slice(1)
1011
1031
 
1012
- gsap.utils.toArray(tSel + ".scroll:not(.scroll-progress)").forEach((el) => {
1032
+ qAll(tSel + ".scroll:not(.scroll-progress)").forEach((el) => {
1013
1033
  if (isReduced(el)) return
1014
1034
  const { delay, duration } = readTiming(el)
1015
1035
  const ease = getEase(el)
@@ -1034,7 +1054,7 @@ export default function initListeners() {
1034
1054
  }))
1035
1055
  })
1036
1056
 
1037
- gsap.utils.toArray(tSel + ".scroll-progress").forEach((el) => {
1057
+ qAll(tSel + ".scroll-progress").forEach((el) => {
1038
1058
  if (isReduced(el)) return
1039
1059
  const ease = getEase(el)
1040
1060
  const parts = getParts(el, getGranularity(el))
@@ -1087,7 +1107,7 @@ export default function initListeners() {
1087
1107
 
1088
1108
  spawnConfigs.forEach((config) => {
1089
1109
  const { sel, typewriter: isTypewriter, typewriterSplit } = config
1090
- gsap.utils.toArray(sel).forEach((el) => {
1110
+ qAll(sel).forEach((el) => {
1091
1111
  if (el.classList.contains("scroll") || el.classList.contains("scroll-progress")) return
1092
1112
  if (isPreserved(el)) return
1093
1113
  if (isReduced(el)) return
@@ -1122,7 +1142,7 @@ export default function initListeners() {
1122
1142
  spawnConfigs.forEach(({ sel, from, typewriter: isTypewriter, text }) => {
1123
1143
  if (isTypewriter || text === false) return
1124
1144
  const tSel = "." + TEXT_PREFIX + sel.slice(1)
1125
- gsap.utils.toArray(tSel).forEach((el) => {
1145
+ qAll(tSel).forEach((el) => {
1126
1146
  if (el.classList.contains("scroll") || el.classList.contains("scroll-progress")) return
1127
1147
  if (isPreserved(el)) return
1128
1148
  if (isReduced(el)) return
@@ -1487,7 +1507,7 @@ export default function initListeners() {
1487
1507
 
1488
1508
  // Bind click + loop animations to every element present at load, tagging
1489
1509
  // them so the MutationObserver below never double-binds a dynamic one.
1490
- gsap.utils.toArray("body *").forEach((el) => {
1510
+ qAll("body *").forEach((el) => {
1491
1511
  if (el.dataset?.gsapSetup) return
1492
1512
  el.dataset.gsapSetup = "1"
1493
1513
  setupClicks(el)
@@ -1608,7 +1628,7 @@ export default function initListeners() {
1608
1628
  appearObserver.observe(document.body, { childList: true, subtree: true })
1609
1629
 
1610
1630
  // Capture any .leave elements already present so they can exit later
1611
- gsap.utils.toArray(".leave").forEach(captureLeave)
1631
+ qAll(".leave").forEach(captureLeave)
1612
1632
 
1613
1633
  const leaveObserver = new MutationObserver((mutations) => {
1614
1634
  mutations.forEach((mutation) => {
@@ -1625,7 +1645,7 @@ export default function initListeners() {
1625
1645
  if (positionTick) return
1626
1646
  positionTick = true
1627
1647
  requestAnimationFrame(() => {
1628
- gsap.utils.toArray(".leave").forEach((el) => {
1648
+ qAll(".leave").forEach((el) => {
1629
1649
  const s = leaveStates.get(el)
1630
1650
  if (s) s.rect = el.getBoundingClientRect()
1631
1651
  })
@@ -1666,7 +1686,7 @@ export default function initListeners() {
1666
1686
  // so dynamically-added .appear elements can animate under the new
1667
1687
  // engine run. data-gsap-preserved (cross-reset memory) and
1668
1688
  // data-gsap-ghost (detached .leave clones) are intentionally KEPT.
1669
- gsap.utils.toArray('[data-gsap-scroll],[data-gsap-setup],[data-gsap-pinned],[data-gsap-scroll-driven]').forEach((el) => {
1689
+ qAll('[data-gsap-scroll],[data-gsap-setup],[data-gsap-pinned],[data-gsap-scroll-driven]').forEach((el) => {
1670
1690
  delete el.dataset.gsapScroll
1671
1691
  delete el.dataset.gsapSetup
1672
1692
  delete el.dataset.gsapPinned
@@ -1681,7 +1701,7 @@ export default function initListeners() {
1681
1701
  document.querySelectorAll('[data-gsap-radiate]').forEach((n) => n.remove())
1682
1702
  cssTweens.forEach((t) => t?.kill())
1683
1703
  cssTweens.length = 0
1684
- gsap.utils.toArray(".typewriter, .scramble").forEach(el => {
1704
+ qAll(".typewriter, .scramble").forEach(el => {
1685
1705
  // Preserved-region typewriters/scrambles stay as-is (already at
1686
1706
  // their end state); finalize the rest so a mid-type/mid-scramble
1687
1707
  // kill can't strand partial or garbled text where the next run's
package/index.d.ts CHANGED
@@ -219,4 +219,8 @@ export function radiate(delay: number, target: TweenTarget, amount: number, dur:
219
219
  export function hover(delay: number, target: TweenTarget, amount: number, dur: number, ease: string): any
220
220
  export function marquee(target: TweenTarget, dir: string, duration: number, xOffset?: number, yOffset?: number, noRepeat?: boolean): any
221
221
  export function flip(state: any, ease: string, dur: number): any
222
- export function animatecss(target: TweenTarget, dur: number, delay: number, ease: string, propertyS: any, propertySValue: any, propertyE: any, propertyEValue: any): any
222
+ export function animatecss(target: TweenTarget, dur: number, delay: number, ease: string, propertyS: any, propertySValue: any, propertyE: any, propertyEValue: any): any
223
+
224
+
225
+ export default function Boot()
226
+
package/index.js CHANGED
@@ -2,4 +2,5 @@ export { initAnimations, toggleAnimations, enableReducedMotion, disableReducedMo
2
2
  export { default as initListeners, registerComplete } from './Listeners.js'
3
3
  export { customAnims } from './CustomAnims.js'
4
4
  export { defaults, animations, normalize } from './Config.js'
5
+ export { Boot } from './Boot.js'
5
6
  export * from './Animations.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gclass-anims",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.16",
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",
@@ -21,6 +21,7 @@
21
21
  "Animations.js",
22
22
  "Config.js",
23
23
  "CustomAnims.js",
24
+ "Boot.js",
24
25
  "README.md",
25
26
  "LICENSE",
26
27
  "CHANGELOG.md"