gclass-anims 1.0.0-beta.2 → 1.0.0-beta.21
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 +199 -7
- package/Animations.js +324 -31
- package/CHANGELOG.md +53 -0
- package/Config.js +43 -5
- package/LICENSE +2 -2
- package/Listeners.js +450 -95
- package/README.md +4 -4
- package/index.d.ts +73 -4
- package/index.js +1 -1
- package/package.json +5 -5
package/AnimToggle.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import initListeners from './Listeners'
|
|
1
|
+
import initListeners from './Listeners.js'
|
|
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 = '
|
|
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 = '
|
|
9
|
+
const REDUCED_KEY = 'gclass-reduced-motion'
|
|
8
10
|
|
|
9
11
|
const reducedMotionQuery = typeof window !== 'undefined'
|
|
10
12
|
? window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
@@ -44,7 +46,7 @@ function readReducedOverride() {
|
|
|
44
46
|
// - Otherwise, if the user HAS an explicit stored choice, respect it
|
|
45
47
|
// (override wins), even under reduced motion.
|
|
46
48
|
// - Otherwise (no stored value) fall back to the default, which is ON unless
|
|
47
|
-
// reduced motion is detected
|
|
49
|
+
// reduced motion is detected - in which case animations are off.
|
|
48
50
|
function getEnabled() {
|
|
49
51
|
if (forcedReduced) return false
|
|
50
52
|
return stored === null ? !reduced : stored
|
|
@@ -111,12 +113,202 @@ 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
|
+
let hasBooted = false // true after first hard-load boot, skips boot on SPA path changes (remains false until first boot, resets on hard reload)
|
|
120
|
+
|
|
121
|
+
// Runtime config for gclassOpts — 0 = defaults (no throttle, default GSAP ticker)
|
|
122
|
+
let currentThrottle = 0
|
|
123
|
+
let currentFps = 0
|
|
124
|
+
const configSubscribers = new Set()
|
|
125
|
+
function getConfigSnapshot() { return { throttlePerFrame: currentThrottle, fps: currentFps } }
|
|
126
|
+
function emitConfig() { configSubscribers.forEach(fn => fn(getConfigSnapshot())) }
|
|
127
|
+
function applyFps(fps) {
|
|
128
|
+
const v = Number(fps) || 0
|
|
129
|
+
currentFps = v
|
|
130
|
+
if (v > 0) gsap.ticker.fps(v)
|
|
131
|
+
else gsap.ticker.fps(0) // 0 = remove cap, fallback to rAF (GSAP default)
|
|
132
|
+
emitConfig()
|
|
133
|
+
}
|
|
134
|
+
function normalizeGclassArgs(throttlePerFrame, fps) {
|
|
135
|
+
// support gclassOpts({throttlePerFrame, fps}) object overload
|
|
136
|
+
if (typeof throttlePerFrame === 'object' && throttlePerFrame !== null) {
|
|
137
|
+
fps = throttlePerFrame.fps
|
|
138
|
+
throttlePerFrame = throttlePerFrame.throttlePerFrame
|
|
139
|
+
}
|
|
140
|
+
const nextThrottle = throttlePerFrame == null ? 0 : Number(throttlePerFrame) || 0
|
|
141
|
+
const nextFps = fps == null ? 0 : Number(fps) || 0
|
|
142
|
+
return { nextThrottle, nextFps }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Change GClass runtime options on the fly without reload.
|
|
147
|
+
* gclassOpts(throttlePerFrame, fps) — both optional numbers.
|
|
148
|
+
* gclassOpts() or gclassOpts(undefined, undefined) resets to defaults (no throttle, default ticker).
|
|
149
|
+
* Also accepts gclassOpts({throttlePerFrame, fps}).
|
|
150
|
+
* Example low-end button: onClick={() => gclassOpts(1, 30)}
|
|
151
|
+
* Example reset: onClick={() => gclassOpts()} // 0, 60fps rAF
|
|
152
|
+
*/
|
|
153
|
+
export function gclassOpts(throttlePerFrame, fps) {
|
|
154
|
+
const { nextThrottle, nextFps } = normalizeGclassArgs(throttlePerFrame, fps)
|
|
155
|
+
currentThrottle = nextThrottle
|
|
156
|
+
applyFps(nextFps)
|
|
157
|
+
// re-wire observers with new throttle without full boot reload (if already running)
|
|
158
|
+
if (typeof window !== 'undefined' && cleanup && !bootTimeout) {
|
|
159
|
+
try { cleanup() } catch {}
|
|
160
|
+
cleanup = null
|
|
161
|
+
if (getEnabled()) cleanup = initListeners(document, currentThrottle)
|
|
162
|
+
}
|
|
163
|
+
// if boot is in progress we just store for next initAnimations; if not running, next initAnimations will use stored values
|
|
164
|
+
return getConfigSnapshot()
|
|
165
|
+
}
|
|
166
|
+
export function getGClassConfig() { return getConfigSnapshot() }
|
|
167
|
+
export function subscribeGClassConfig(cb) {
|
|
168
|
+
configSubscribers.add(cb)
|
|
169
|
+
return () => configSubscribers.delete(cb)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const readBootTime = (els, fallback) => {
|
|
173
|
+
let max = null
|
|
174
|
+
for (const el of els) {
|
|
175
|
+
const cls = [...el.classList].find(c => c.startsWith('boot-time-'))
|
|
176
|
+
if (cls) {
|
|
177
|
+
const n = Number(cls.slice('boot-time-'.length))
|
|
178
|
+
if (!Number.isNaN(n)) max = max === null ? n : Math.max(max, n)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return max ?? fallback
|
|
182
|
+
}
|
|
114
183
|
|
|
115
184
|
// Boots the GSAP animation system unless animations are disabled (stored "off"
|
|
116
185
|
// or reduced-motion fallback with no explicit choice). Idempotent: calling it
|
|
117
186
|
// again tears down any previous run first.
|
|
118
|
-
|
|
187
|
+
// Now also handles boot screen: any HTML/JSX with `.boot-up` anywhere is treated as the boot overlay.
|
|
188
|
+
// No separate initBoot needed - just call initAnimations().
|
|
189
|
+
// Boot stops all DOM rendering for defaults.bootTime (overwritten by boot-time-N class).
|
|
190
|
+
// throttlePerFrame / fps: forwarded to gclassOpts-equivalent runtime.
|
|
191
|
+
// initAnimations(throttlePerFrame, fps) — positional numbers, 0/undefined = defaults
|
|
192
|
+
// initAnimations({throttlePerFrame, fps}) — object overload
|
|
193
|
+
// initAnimations() — uses last gclassOpts values (defaults on first call)
|
|
194
|
+
export function initAnimations(throttlePerFrame, fps) {
|
|
195
|
+
// gclassOpts-style normalization: (throttle, fps) positional or {throttlePerFrame, fps} object
|
|
196
|
+
// no args -> fallback to last gclassOpts values (defaults 0 = no throttle, default ticker)
|
|
197
|
+
let effThrottle = currentThrottle
|
|
198
|
+
let effFps = currentFps
|
|
199
|
+
if (throttlePerFrame !== undefined || fps !== undefined) {
|
|
200
|
+
const { nextThrottle, nextFps } = normalizeGclassArgs(throttlePerFrame, fps)
|
|
201
|
+
effThrottle = nextThrottle
|
|
202
|
+
effFps = nextFps
|
|
203
|
+
currentThrottle = effThrottle
|
|
204
|
+
currentFps = effFps
|
|
205
|
+
if (effFps > 0) gsap.ticker.fps(effFps)
|
|
206
|
+
else gsap.ticker.fps(0)
|
|
207
|
+
emitConfig()
|
|
208
|
+
} else {
|
|
209
|
+
if (effFps > 0) gsap.ticker.fps(effFps)
|
|
210
|
+
else gsap.ticker.fps(0)
|
|
211
|
+
}
|
|
119
212
|
if (typeof window === 'undefined' || !getEnabled()) return
|
|
120
|
-
|
|
121
|
-
|
|
213
|
+
// boot already in progress (first mount in StrictMode) - ignore second mount
|
|
214
|
+
if (bootTimeout) {
|
|
215
|
+
console.log(`[initAnimations] boot already in progress - ignoring duplicate call`)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
if (cleanup) { cleanup(); cleanup = null }
|
|
219
|
+
if (bootCleanup) { bootCleanup(); bootCleanup = null }
|
|
220
|
+
if (bootStyle) { bootStyle.remove(); bootStyle = null; document.documentElement.classList.remove('gclass-booting') }
|
|
221
|
+
|
|
222
|
+
const hideBootEls = (els) => {
|
|
223
|
+
// React-safe: don't el.remove() - React owns the nodes and will throw
|
|
224
|
+
// insertBefore/removeChild on next commit if we mutate outside React.
|
|
225
|
+
// Hiding keeps React's tree intact but visually removes boot screen.
|
|
226
|
+
els.forEach(el => {
|
|
227
|
+
el.style.display = 'none'
|
|
228
|
+
el.setAttribute('hidden', '')
|
|
229
|
+
el.setAttribute('data-gclass-boot-hidden', '1')
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const bootEls = typeof document !== 'undefined' ? [...document.querySelectorAll(".boot-up")].filter(el => !el.hasAttribute('data-gclass-boot-hidden')) : []
|
|
234
|
+
if (!bootEls.length) {
|
|
235
|
+
// no .boot-up -> completely skip boot
|
|
236
|
+
} else if (bootEls.length > 1) {
|
|
237
|
+
console.error(`[initAnimations] Multiple .boot-up elements detected (${bootEls.length}) - skipping all boot animations`, bootEls)
|
|
238
|
+
hideBootEls(bootEls)
|
|
239
|
+
hasBooted = true
|
|
240
|
+
// fall through to normal initListeners without pausing DOM
|
|
241
|
+
} else if (hasBooted && !bootTimeout) {
|
|
242
|
+
// path change after already booted (SPA navigation) - skip boot, hard reload resets hasBooted
|
|
243
|
+
console.log(`[initAnimations] skipping boot on path change (already booted)`, bootEls)
|
|
244
|
+
hideBootEls(bootEls)
|
|
245
|
+
hasBooted = true
|
|
246
|
+
// fall through
|
|
247
|
+
} else {
|
|
248
|
+
const bootEl = bootEls[0]
|
|
249
|
+
const hasBootEnd = [...bootEl.classList].some(c => c.startsWith('boot-end-'))
|
|
250
|
+
if (!hasBootEnd) {
|
|
251
|
+
// no boot-end-* -> completely skip boot animation (still pause? spec says skip it)
|
|
252
|
+
// 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"
|
|
253
|
+
// interpret as skip the exit animation only, still pause for bootTime
|
|
254
|
+
// To match "completely skip it" for boot-end, we just don't play exit tween
|
|
255
|
+
}
|
|
256
|
+
const bootTime = readBootTime(bootEls, defaults.bootTime ?? 2)
|
|
257
|
+
console.log(`[initAnimations] .boot-up found: ${bootEls.length} - pausing DOM for ${bootTime}s`, bootEls)
|
|
258
|
+
hasBooted = true
|
|
259
|
+
// stop all DOM rendering except .boot-up
|
|
260
|
+
bootStyle = document.createElement('style')
|
|
261
|
+
bootStyle.id = 'gclass-boot-style'
|
|
262
|
+
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}`
|
|
263
|
+
document.head.appendChild(bootStyle)
|
|
264
|
+
document.documentElement.classList.add('gclass-booting')
|
|
265
|
+
// ensure boot els are visible even if nested inside hidden ancestors
|
|
266
|
+
bootEls.forEach(el => { el.style.visibility = 'visible' })
|
|
267
|
+
|
|
268
|
+
// animations inside boot screen must play while rest of DOM is hidden - init scoped to boot-up
|
|
269
|
+
bootCleanup = initListeners(bootEl, effThrottle)
|
|
270
|
+
|
|
271
|
+
bootTimeout = setTimeout(() => {
|
|
272
|
+
const bootEndCls = [...bootEl.classList].find(c => c.startsWith('boot-end-'))
|
|
273
|
+
if (!bootEndCls) {
|
|
274
|
+
// no boot-end -> skip exit animation, just hide
|
|
275
|
+
bootCleanup?.(); bootCleanup = null
|
|
276
|
+
document.documentElement.classList.remove('gclass-booting')
|
|
277
|
+
bootStyle?.remove(); bootStyle = null
|
|
278
|
+
hideBootEls(bootEls)
|
|
279
|
+
bootTimeout = null
|
|
280
|
+
cleanup = initListeners(document, effThrottle)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
const name = bootEndCls.slice('boot-end-'.length) // e.g. spawn-blur
|
|
284
|
+
const cfg = animations.find(a => a.sel === '.' + name)
|
|
285
|
+
const from = cfg?.from
|
|
286
|
+
const easeCl = [...bootEl.classList].find(c => c.startsWith('ease-'))
|
|
287
|
+
const ease = easeCl ? easeCl.split('-')[1] : defaults.ease
|
|
288
|
+
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
|
|
289
|
+
// Actually use boot-end-time-N if present, else effectDuration
|
|
290
|
+
const endTimeCls = [...bootEl.classList].find(c => c.startsWith('boot-end-time-'))
|
|
291
|
+
const endDur = endTimeCls ? Number(endTimeCls.slice('boot-end-time-'.length)) : dur
|
|
292
|
+
|
|
293
|
+
const finish = () => {
|
|
294
|
+
bootCleanup?.(); bootCleanup = null
|
|
295
|
+
document.documentElement.classList.remove('gclass-booting')
|
|
296
|
+
bootStyle?.remove(); bootStyle = null
|
|
297
|
+
hideBootEls(bootEls)
|
|
298
|
+
bootTimeout = null
|
|
299
|
+
cleanup = initListeners(document, effThrottle)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!cfg || !from) {
|
|
303
|
+
console.warn(`[initAnimations] boot-end-${name} has no from state - removing without animation`)
|
|
304
|
+
finish()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
// play spawn in reverse (visible -> hidden) before removing
|
|
308
|
+
gsap.to(bootEl, { ...from, duration: endDur, ease, onComplete: finish })
|
|
309
|
+
}, bootTime * 1000)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
cleanup = initListeners(document, effThrottle)
|
|
122
314
|
}
|