gclass-anims 1.0.0-beta.1
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 +122 -0
- package/Animations.js +436 -0
- package/Config.js +160 -0
- package/CustomAnims.js +56 -0
- package/LICENSE +35 -0
- package/Listeners.js +1464 -0
- package/README.md +98 -0
- package/index.d.ts +180 -0
- package/index.js +5 -0
- package/package.json +50 -0
package/AnimToggle.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import initListeners from './Listeners'
|
|
2
|
+
|
|
3
|
+
// localStorage key controlling whether the GSAP animation system is mounted.
|
|
4
|
+
const STORAGE_KEY = 'funbyte-animations-enabled'
|
|
5
|
+
// localStorage key for a forced reduced-motion override (see
|
|
6
|
+
// enableReducedMotion / disableReducedMotion).
|
|
7
|
+
const REDUCED_KEY = 'funbyte-reduced-motion'
|
|
8
|
+
|
|
9
|
+
const reducedMotionQuery = typeof window !== 'undefined'
|
|
10
|
+
? window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
11
|
+
: null
|
|
12
|
+
|
|
13
|
+
// Module-level singleton store so anything subscribing to it stays in sync.
|
|
14
|
+
let stored = readStored()
|
|
15
|
+
let reduced = reducedMotionQuery?.matches ?? false
|
|
16
|
+
let forcedReduced = readReducedOverride()
|
|
17
|
+
const subscribers = new Set()
|
|
18
|
+
|
|
19
|
+
// Returns the stored preference, or null when the user has never explicitly
|
|
20
|
+
// chosen (no localStorage value). Distinct from a boolean so we can tell
|
|
21
|
+
// "user override" apart from "use the default".
|
|
22
|
+
function readStored() {
|
|
23
|
+
if (typeof window === 'undefined') return null
|
|
24
|
+
try {
|
|
25
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
26
|
+
return raw === null ? null : raw === 'true'
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Returns whether animations were force-disabled via enableReducedMotion().
|
|
33
|
+
function readReducedOverride() {
|
|
34
|
+
if (typeof window === 'undefined') return false
|
|
35
|
+
try {
|
|
36
|
+
return localStorage.getItem(REDUCED_KEY) === 'true'
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Enabled rule:
|
|
43
|
+
// - A forced reduced-motion override always wins (animations off).
|
|
44
|
+
// - Otherwise, if the user HAS an explicit stored choice, respect it
|
|
45
|
+
// (override wins), even under reduced motion.
|
|
46
|
+
// - Otherwise (no stored value) fall back to the default, which is ON unless
|
|
47
|
+
// reduced motion is detected — in which case animations are off.
|
|
48
|
+
function getEnabled() {
|
|
49
|
+
if (forcedReduced) return false
|
|
50
|
+
return stored === null ? !reduced : stored
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function emit() {
|
|
54
|
+
subscribers.forEach((fn) => fn(getEnabled()))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// React to the OS reduced-motion setting live (no reload). Toggling off
|
|
58
|
+
// reverts tweens; toggling back on re-applies them.
|
|
59
|
+
if (reducedMotionQuery) {
|
|
60
|
+
reducedMotionQuery.addEventListener('change', () => {
|
|
61
|
+
reduced = reducedMotionQuery.matches
|
|
62
|
+
emit()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function subscribe(cb) {
|
|
67
|
+
subscribers.add(cb)
|
|
68
|
+
return () => subscribers.delete(cb)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getSnapshot() {
|
|
72
|
+
return getEnabled()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Persists the new value, then reloads the page so the change applies cleanly.
|
|
76
|
+
export function toggleAnimations() {
|
|
77
|
+
stored = !getEnabled()
|
|
78
|
+
try {
|
|
79
|
+
localStorage.setItem(STORAGE_KEY, String(stored))
|
|
80
|
+
} catch {
|
|
81
|
+
/* ignore storage errors (private mode etc.) */
|
|
82
|
+
}
|
|
83
|
+
emit()
|
|
84
|
+
if (typeof window !== 'undefined') window.location.reload()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Force animations off (a hard override that wins over any stored preference),
|
|
88
|
+
// then reload so the running instance tears down. Use disableReducedMotion() to
|
|
89
|
+
// clear it.
|
|
90
|
+
export function enableReducedMotion() {
|
|
91
|
+
forcedReduced = true
|
|
92
|
+
try {
|
|
93
|
+
localStorage.setItem(REDUCED_KEY, 'true')
|
|
94
|
+
} catch {
|
|
95
|
+
/* ignore storage errors */
|
|
96
|
+
}
|
|
97
|
+
emit()
|
|
98
|
+
if (typeof window !== 'undefined') window.location.reload()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Clear the forced reduced-motion override (if any), then reload.
|
|
102
|
+
export function disableReducedMotion() {
|
|
103
|
+
forcedReduced = false
|
|
104
|
+
try {
|
|
105
|
+
localStorage.removeItem(REDUCED_KEY)
|
|
106
|
+
} catch {
|
|
107
|
+
/* ignore storage errors */
|
|
108
|
+
}
|
|
109
|
+
emit()
|
|
110
|
+
if (typeof window !== 'undefined') window.location.reload()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let cleanup = null
|
|
114
|
+
|
|
115
|
+
// Boots the GSAP animation system unless animations are disabled (stored "off"
|
|
116
|
+
// or reduced-motion fallback with no explicit choice). Idempotent: calling it
|
|
117
|
+
// again tears down any previous run first.
|
|
118
|
+
export function initAnimations() {
|
|
119
|
+
if (typeof window === 'undefined' || !getEnabled()) return
|
|
120
|
+
if (cleanup) cleanup()
|
|
121
|
+
cleanup = initListeners()
|
|
122
|
+
}
|
package/Animations.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { Flip, SplitText, TextPlugin } from "gsap/all";
|
|
2
|
+
import gsap from "gsap";
|
|
3
|
+
|
|
4
|
+
gsap.registerPlugin(Flip)
|
|
5
|
+
gsap.registerPlugin(SplitText)
|
|
6
|
+
gsap.registerPlugin(TextPlugin)
|
|
7
|
+
|
|
8
|
+
// A tasteful fallback whenever a call site omits an ease, so the animation
|
|
9
|
+
// never lapses into the raw "none" look. Callers still override this freely.
|
|
10
|
+
const DEFAULT_EASE = "power3.out";
|
|
11
|
+
const easeOf = (e) => e || DEFAULT_EASE;
|
|
12
|
+
|
|
13
|
+
// These animations treat `amount` as a scale multiplier, but dampened by a
|
|
14
|
+
// factor of 10: `amount-20` scales to 2x rather than 20x. The target scale is
|
|
15
|
+
// `amount / 10`, so use `amount-N` as a percentage-style number.
|
|
16
|
+
|
|
17
|
+
// The resting opacity the element should settle on. GSAP sets the spawn's
|
|
18
|
+
// `from` state (opacity 0) on the element before the tween is built, so
|
|
19
|
+
// `getComputedStyle` would read that transient 0 rather than the intended
|
|
20
|
+
// value. Temporarily drop the inline opacity to read the CSS-defined one (e.g.
|
|
21
|
+
// a `disabled:opacity-50`, a `.opacity-*` utility, or the default 1), then
|
|
22
|
+
// restore it so the actual animation still starts from the right place.
|
|
23
|
+
export const finalOpacity = (target) => {
|
|
24
|
+
const had = target.style.opacity
|
|
25
|
+
target.style.removeProperty("opacity")
|
|
26
|
+
const v = parseFloat(getComputedStyle(target).opacity)
|
|
27
|
+
if (had !== "") target.style.opacity = had
|
|
28
|
+
return isNaN(v) ? 1 : v
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
//Spawn animations
|
|
33
|
+
|
|
34
|
+
export function SpawnV (target , delay , dir , dur , ease) {
|
|
35
|
+
const e = easeOf(ease)
|
|
36
|
+
// A whisper of scale in the same breath as the travel keeps the reveal
|
|
37
|
+
// feeling physical instead of a flat 2D slide.
|
|
38
|
+
return gsap.fromTo(target , {opacity:0 , y:dir , scale:0.97} , {ease:e , duration:dur , delay:delay , y:0 , opacity:finalOpacity(target) , scale:1})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function SpawnH (target , delay , dir , dur , ease) {
|
|
42
|
+
const e = easeOf(ease)
|
|
43
|
+
return gsap.fromTo(target , {opacity:0 , x:dir , scale:0.97} , {ease:e , duration:dur , delay:delay , x:0 , opacity:finalOpacity(target) , scale:1})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expandV (target , delay , dur , ease){
|
|
47
|
+
const e = easeOf(ease)
|
|
48
|
+
return gsap.fromTo(target , {opacity:1 , scaleY:0} , {ease:e , duration:dur , delay:delay , scaleY:1 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function expandH (target , delay , dur , ease){
|
|
52
|
+
const e = easeOf(ease)
|
|
53
|
+
return gsap.fromTo(target , {opacity:1 , scaleX:0} , {ease:e , duration:dur , delay:delay , scaleX:1 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
54
|
+
}
|
|
55
|
+
export function expandA (target , delay , dur , ease){
|
|
56
|
+
const e = easeOf(ease)
|
|
57
|
+
return gsap.fromTo(target , {opacity:1 , scale:0} , {ease:e , duration:dur , delay:delay , scale:1 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function typewriter (target , text , dur , delay , ease){
|
|
61
|
+
return gsap.fromTo(target , {text:""} , {ease:easeOf(ease) , duration:dur , delay:delay , text:text})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function spawnSpinCCW (target , delay , dur , ease){
|
|
65
|
+
const e = easeOf(ease)
|
|
66
|
+
return gsap.fromTo(target , {scale:0 , rotation:-90} , {ease:e , duration:dur , delay:delay , scale:1 , rotation:0 , transformOrigin:"50% 50%"})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function spawnSpinCW (target , delay , dur , ease){
|
|
70
|
+
const e = easeOf(ease)
|
|
71
|
+
return gsap.fromTo(target , {scale:0 , rotation:90} , {ease:e , duration:dur , delay:delay , scale:1 , rotation:0 , transformOrigin:"50% 50%"})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function spawnFade (target , delay , dur , ease){
|
|
75
|
+
return gsap.fromTo(target , {opacity:0} , {ease:easeOf(ease) , duration:dur , delay:delay , opacity:finalOpacity(target)})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function spawnBlur (target , delay , dur , ease){
|
|
79
|
+
const e = easeOf(ease)
|
|
80
|
+
// Linger the blur slightly so the focus pull feels deliberate, not abrupt.
|
|
81
|
+
return gsap.fromTo(target , {opacity:0 , filter:"blur(20px)"} , {ease:e , duration:dur , delay:delay , opacity:finalOpacity(target) , filter:"blur(0px)"})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clip-path reveal: the element's box is wiped open from a chosen edge. `dir`
|
|
85
|
+
// is one of up/down/left/right and picks which inset collapses to zero so the
|
|
86
|
+
// wipe travels from that edge:
|
|
87
|
+
// up - hidden at the bottom, wipes open upward (bottom -> top)
|
|
88
|
+
// down - hidden at the top, wipes open downward (top -> bottom)
|
|
89
|
+
// left - hidden on the right, wipes open leftward (right -> left)
|
|
90
|
+
// right - hidden on the left, wipes open rightward (left -> right)
|
|
91
|
+
// The `from` inset is mirrored in the Config entry so
|
|
92
|
+
// `.scroll`/`.scroll-progress`/`.leave` reversal and `.appear` all know the
|
|
93
|
+
// hidden state. No opacity is involved — pure clip wipe.
|
|
94
|
+
const CLIP_FROM = {
|
|
95
|
+
up: "inset(0% 0% 100% 0%)",
|
|
96
|
+
down: "inset(100% 0% 0% 0%)",
|
|
97
|
+
left: "inset(0% 0% 0% 100%)",
|
|
98
|
+
right: "inset(0% 100% 0% 0%)",
|
|
99
|
+
}
|
|
100
|
+
export function spawnClipReveal (target , delay , dur , ease , dir = "up"){
|
|
101
|
+
const from = CLIP_FROM[dir] || CLIP_FROM.up
|
|
102
|
+
return gsap.fromTo(target , {clipPath: from} , {clipPath:"inset(0% 0% 0% 0%)" , ease:easeOf(ease) , duration:dur , delay:delay})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Curtain reveal: opens outward from the horizontal centre — a vertical slit in
|
|
106
|
+
// the middle widens left and right until the whole box is shown.
|
|
107
|
+
export function curtainHorizontal (target , delay , dur , ease){
|
|
108
|
+
return gsap.fromTo(target , {clipPath:"inset(0% 50% 0% 50%)"} , {clipPath:"inset(0% 0% 0% 0%)" , ease:easeOf(ease) , duration:dur , delay:delay})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Curtain reveal: opens outward from the vertical centre — a horizontal slit in
|
|
112
|
+
// the middle widens up and down until the whole box is shown.
|
|
113
|
+
export function curtainVertical (target , delay , dur , ease){
|
|
114
|
+
return gsap.fromTo(target , {clipPath:"inset(50% 0% 50% 0%)"} , {clipPath:"inset(0% 0% 0% 0%)" , ease:easeOf(ease) , duration:dur , delay:delay})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function spawnXUp (target , delay , dur , ease){
|
|
118
|
+
const e = easeOf(ease)
|
|
119
|
+
// Card-spawn: a full 360° front-flip that unfolds into place, with a soft
|
|
120
|
+
// depth scale and an edge anchor so the pivot reads like a flipping card.
|
|
121
|
+
return gsap.fromTo(target , {scale:0 , rotationX:360 , opacity:0} , {ease:e , duration:dur , delay:delay , scale:1 , rotationX:0 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function spawnXDown (target , delay , dur , ease){
|
|
125
|
+
const e = easeOf(ease)
|
|
126
|
+
return gsap.fromTo(target , {scale:0 , rotationX:-360 , opacity:0} , {ease:e , duration:dur , delay:delay , scale:1 , rotationX:0 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function spawnYRight (target , delay , dur , ease){
|
|
130
|
+
const e = easeOf(ease)
|
|
131
|
+
return gsap.fromTo(target , {scale:0 , rotationY:360 , opacity:0} , {ease:e , duration:dur , delay:delay , scale:1 , rotationY:0 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function spawnYLeft (target , delay , dur , ease){
|
|
135
|
+
const e = easeOf(ease)
|
|
136
|
+
return gsap.fromTo(target , {scale:0 , rotationY:-360 , opacity:0} , {ease:e , duration:dur , delay:delay , scale:1 , rotationY:0 , opacity:finalOpacity(target) , transformOrigin:"50% 50%"})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function expandRight (target , delay , dur , ease){
|
|
140
|
+
const e = easeOf(ease)
|
|
141
|
+
const tl = gsap.timeline()
|
|
142
|
+
tl.set(target , {transformOrigin : "100% 50%"})
|
|
143
|
+
.fromTo(target , {opacity:1 , scaleX:0} , {ease:e , duration:dur , delay:delay , scaleX:1 , opacity:finalOpacity(target)})
|
|
144
|
+
|
|
145
|
+
return tl
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function expandLeft (target , delay , dur , ease){
|
|
150
|
+
const e = easeOf(ease)
|
|
151
|
+
const tl = gsap.timeline()
|
|
152
|
+
tl.set(target , {transformOrigin : "0% 50%"})
|
|
153
|
+
.fromTo(target , {opacity:1 , scaleX:0} , {ease:e , duration:dur , delay:delay , scaleX:1 , opacity:finalOpacity(target)})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
return tl
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function expandUp (target , delay , dur , ease){
|
|
160
|
+
const e = easeOf(ease)
|
|
161
|
+
const tl = gsap.timeline()
|
|
162
|
+
tl.set(target , {transformOrigin : "50% 100%"})
|
|
163
|
+
.fromTo(target , {opacity:1 , scaleY:0} , {ease:e , duration:dur , delay:delay , scaleY:1 , opacity:finalOpacity(target)})
|
|
164
|
+
|
|
165
|
+
return tl
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function expandDown (target , delay , dur , ease){
|
|
169
|
+
const e = easeOf(ease)
|
|
170
|
+
const tl = gsap.timeline()
|
|
171
|
+
tl.set(target , {transformOrigin : "50% 0%"})
|
|
172
|
+
.fromTo(target , {opacity:1 , scaleY:0} , {ease:e , duration:dur , delay:delay , scaleY:1 , opacity:finalOpacity(target)})
|
|
173
|
+
|
|
174
|
+
return tl
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function countTargetVars (target){
|
|
178
|
+
// Cache on the element so the target number survives a `.scroll` reverse
|
|
179
|
+
// (which rewrites textContent back to the start value). Re-reading it from
|
|
180
|
+
// the live text each play would otherwise collapse the range to start==end.
|
|
181
|
+
if (target._countTarget) return target._countTarget
|
|
182
|
+
// Extract the number from the element, ignoring any surrounding text
|
|
183
|
+
// (e.g. "$1,250 total" -> 1250). Keeps decimals so 3.14 counts to 3.14.
|
|
184
|
+
const match = (target.textContent || "0").replace(/,/g, "").match(/-?\d+(?:\.\d+)?/)
|
|
185
|
+
const end = match ? parseFloat(match[0]) : 0
|
|
186
|
+
const decimals = match?.[0].includes(".") ? (match[0].split(".")[1] || "").length : 0
|
|
187
|
+
// Count FROM `.spawn-num-N` (N = the starting number) up to `end`. When the
|
|
188
|
+
// class is absent, fall back to 0.
|
|
189
|
+
const startCls = [...target.classList].find(c => c.startsWith("spawn-num-"))
|
|
190
|
+
const start = startCls ? parseFloat(startCls.slice("spawn-num-".length)) : 0
|
|
191
|
+
return target._countTarget = { start, end, decimals }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function countUp (target , delay , dur, ease){
|
|
195
|
+
const e = easeOf(ease)
|
|
196
|
+
const { start, end, decimals } = countTargetVars(target)
|
|
197
|
+
const obj = { n: start }
|
|
198
|
+
// Pure counter: animates the number only, leaving opacity untouched, so it
|
|
199
|
+
// composes cleanly with `.scroll` / `.scroll-progress` / `.appear` /
|
|
200
|
+
// `.leave` without imposing a fade.
|
|
201
|
+
return gsap.timeline({ delay }).fromTo(obj , { n: start } , { n: end , duration:dur , ease:e , onUpdate: () => { target.textContent = obj.n.toFixed(decimals) } } , 0)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
//Mouse animations
|
|
206
|
+
|
|
207
|
+
export function verticalmove (target , amount , dur , ease){
|
|
208
|
+
const e = easeOf(ease)
|
|
209
|
+
// A hint of scale makes the lift read as pressing/pulling rather than a
|
|
210
|
+
// detached translate, and easing by weight keeps it from feeling springy.
|
|
211
|
+
return gsap.to(target , {y:amount , scale:1 + amount / 1000 , duration:dur , ease:e})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function expandmove (target , amount , dur , ease){
|
|
215
|
+
// `amount` uses the dampened `amount-N` scale system: the target scale is
|
|
216
|
+
// `amount / 10`. Rest state (scale 1) is therefore amount = 10.
|
|
217
|
+
return gsap.to(target , {scale:amount / 10 , duration:dur , ease:easeOf(ease)})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function magnet (target , x , y , scale , dur , ease){
|
|
221
|
+
// Drives a cursor-attracted element: translate toward the given point while
|
|
222
|
+
// scaling up. `overwrite:"auto"` kills the in-flight tween on every
|
|
223
|
+
// mousemove so motion stays snappy instead of queuing up behind itself.
|
|
224
|
+
// Passing x:0, y:0, scale:1 resets it back to rest on mouseleave.
|
|
225
|
+
return gsap.to(target , {x , y , scale , duration:dur , ease:easeOf(ease) , overwrite:"auto"})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function magnet3d (target , x , y , scale , rotX , rotY , dur , ease){
|
|
229
|
+
// Like `magnet` but also tilts the element in 3D space to face the cursor:
|
|
230
|
+
// translation pulls it toward the pointer while rotationX/rotationY lean it
|
|
231
|
+
// so the face tracks the cursor. `transformOrigin:"50% 50%"` keeps the tilt
|
|
232
|
+
// pivoting around the element's centre and `transformPerspective` gives the
|
|
233
|
+
// rotation its depth (without it, rotationX/rotationY on a flat element look
|
|
234
|
+
// like a subtle skew rather than a real 3D tilt). Passing x:0, y:0, scale:1,
|
|
235
|
+
// rotX:0, rotY:0 resets it back to rest on mouseleave.
|
|
236
|
+
return gsap.to(target , {x , y , scale , rotationX:rotX , rotationY:rotY , transformOrigin:"50% 50%" , transformPerspective:600 , duration:dur , ease:easeOf(ease) , overwrite:"auto"})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function reset (target , dur , ease){
|
|
240
|
+
gsap.to(target, { x: 0, y: 0, scale: 1, duration: dur/2, ease: ease })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
//Loop animations
|
|
247
|
+
|
|
248
|
+
export function spinCW (target , delay , dur , ease){
|
|
249
|
+
const e = easeOf(ease)
|
|
250
|
+
const tl = gsap.timeline()
|
|
251
|
+
tl.fromTo(target , {scale:1 , rotation:0} , {ease:e , duration:dur , delay:delay , rotation:360})
|
|
252
|
+
.to(target , {scale:1 , duration: delay})
|
|
253
|
+
return tl
|
|
254
|
+
}
|
|
255
|
+
export function spinCCW (target , delay , dur , ease){
|
|
256
|
+
const e = easeOf(ease)
|
|
257
|
+
const tl = gsap.timeline()
|
|
258
|
+
tl.fromTo(target , {scale:1 , rotation:0} , {ease:e , duration:dur , delay:delay , rotation:-360})
|
|
259
|
+
.to(target , {scale:1 , duration: delay})
|
|
260
|
+
return tl
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function bounce (delay , target , amount , dur , ease){
|
|
264
|
+
const e = easeOf(ease)
|
|
265
|
+
const tl = gsap.timeline()
|
|
266
|
+
tl.set(target , {transformOrigin : "50% 100%"})
|
|
267
|
+
// A quick, punchy hop: one continuous eased rise to the crest (no
|
|
268
|
+
// intermediate tween to decelerate into, so it can't hang mid-air), then a
|
|
269
|
+
// weighted squash-and-settle on the way down.
|
|
270
|
+
.to(target , {y:-amount - amount * 0.25 , scaleX:0.97 , scaleY:1.03 , duration:dur * 0.48 , ease:"power1.out"})
|
|
271
|
+
.to(target , {y:0 , scaleX:1.08 , scaleY:0.88 , duration:dur * 0.3 , ease:"power2.in"})
|
|
272
|
+
.to(target , {y:0 , scaleX:1 , scaleY:1 , duration:dur * 0.12 , ease:e})
|
|
273
|
+
|
|
274
|
+
.to(target , {x:0 , duration:delay})
|
|
275
|
+
|
|
276
|
+
return tl
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
export function shake (delay , target , amount , dur , ease){
|
|
281
|
+
const tl = gsap.timeline()
|
|
282
|
+
// Damped oscillation: each swing decays so it feels like real inertia
|
|
283
|
+
// settling, rather than a metronome ticking back and forth.
|
|
284
|
+
tl.to(target , {x:amount , duration:dur * 0.1 , ease:"power2.out"})
|
|
285
|
+
.to(target , {x:-amount * 0.8, duration:dur * 0.15 , ease:"power2.inOut"})
|
|
286
|
+
.to(target , {x:amount * 0.5, duration:dur * 0.2 , ease:"power2.inOut"})
|
|
287
|
+
.to(target , {x:-amount * 0.25, duration:dur * 0.2 , ease:"power2.inOut"})
|
|
288
|
+
.to(target , {x:0 , duration:dur * 0.2 , ease:easeOf(ease)})
|
|
289
|
+
.to(target , {x:0 , duration:delay})
|
|
290
|
+
|
|
291
|
+
return tl
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
export function bell (delay , target , amount , dur , ease){
|
|
296
|
+
const tl = gsap.timeline()
|
|
297
|
+
tl.set(target , {transformOrigin : "50% 0%"})
|
|
298
|
+
// A quick toll that overshoots and damps down — reads as a physical strike
|
|
299
|
+
// instead of a symmetrical wiggle.
|
|
300
|
+
.to(target , {rotate:amount , duration:dur * 0.12 , ease:"power2.out"})
|
|
301
|
+
.to(target , {rotate:-amount * 0.7, duration:dur * 0.18 , ease:"power2.inOut"})
|
|
302
|
+
.to(target , {rotate:amount * 0.4, duration:dur * 0.22 , ease:"power2.inOut"})
|
|
303
|
+
.to(target , {rotate:-amount * 0.15, duration:dur * 0.22 , ease:"power2.inOut"})
|
|
304
|
+
.to(target , {rotate:0 , duration:dur * 0.2 , ease:easeOf(ease)})
|
|
305
|
+
.to(target , {rotate:0 , duration:delay})
|
|
306
|
+
|
|
307
|
+
return tl
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function pulse (delay , target , amount , dur , ease){
|
|
311
|
+
const e = easeOf(ease)
|
|
312
|
+
const tl = gsap.timeline()
|
|
313
|
+
// Overshoot a touch past the target then fall back, so the pulse has a
|
|
314
|
+
// lively beat rather than a flat up-and-down.
|
|
315
|
+
tl.to(target , {scale:(amount / 10) * 1.05 , duration:dur * 0.35 , ease:"power2.out"})
|
|
316
|
+
.to(target , {scale:1 , duration:dur * 0.45 , ease:e})
|
|
317
|
+
|
|
318
|
+
.to(target , {scale:1 , duration:delay})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
return tl
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function radiate (delay , target , amount , dur , ease , zIndex){
|
|
325
|
+
const clone = target.cloneNode(true)
|
|
326
|
+
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};` : ""}`
|
|
327
|
+
// Keep the ripple glued to the target so it tracks scroll/resize instead of
|
|
328
|
+
// getting stranded at the position captured when the animation was built.
|
|
329
|
+
// Reposition on scroll/resize (throttled to one pass per frame) rather than
|
|
330
|
+
// reading the rect on EVERY tick: the latter forces a synchronous reflow per
|
|
331
|
+
// frame, a layout-thrash Firefox pays for far more heavily than Chromium.
|
|
332
|
+
let tick = false
|
|
333
|
+
const applyRect = () => {
|
|
334
|
+
const r = target.getBoundingClientRect()
|
|
335
|
+
clone.style.left = `${r.left}px`
|
|
336
|
+
clone.style.top = `${r.top}px`
|
|
337
|
+
clone.style.width = `${r.width}px`
|
|
338
|
+
clone.style.height = `${r.height}px`
|
|
339
|
+
}
|
|
340
|
+
const schedule = () => {
|
|
341
|
+
if (tick) return
|
|
342
|
+
tick = true
|
|
343
|
+
requestAnimationFrame(() => { tick = false; applyRect() })
|
|
344
|
+
}
|
|
345
|
+
document.body.appendChild(clone)
|
|
346
|
+
applyRect()
|
|
347
|
+
window.addEventListener("scroll", schedule, { passive: true })
|
|
348
|
+
window.addEventListener("resize", schedule, { passive: true })
|
|
349
|
+
|
|
350
|
+
return gsap.fromTo(clone , {scale:1 , opacity:1} , {
|
|
351
|
+
scale:amount / 10 ,
|
|
352
|
+
opacity:0 ,
|
|
353
|
+
duration:dur ,
|
|
354
|
+
delay:delay ,
|
|
355
|
+
ease:easeOf(ease) ,
|
|
356
|
+
onComplete: () => {
|
|
357
|
+
clone.remove()
|
|
358
|
+
window.removeEventListener("scroll", schedule)
|
|
359
|
+
window.removeEventListener("resize", schedule)
|
|
360
|
+
} ,
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
export function hover (delay , target , amount , dur , ease){
|
|
366
|
+
const tl = gsap.timeline()
|
|
367
|
+
// Smooth, symmetric drift up and back down reads as floating rather than
|
|
368
|
+
// bouncing. A gentle sine ease plus a soft scale breathing keeps it alive.
|
|
369
|
+
tl.set(target , {transformOrigin : "50% 50%"})
|
|
370
|
+
.to(target , {y:-amount , scaleX:0.98 , scaleY:1.02 , duration:dur/2 , ease:"sine.inOut"})
|
|
371
|
+
.to(target , {y:0 , scaleX:1 , scaleY:1 , duration:dur/2 , ease:"sine.inOut"})
|
|
372
|
+
|
|
373
|
+
return tl
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
export function marquee (target , dir , duration , xOffset = 0 , yOffset = 0 , noRepeat = false){
|
|
378
|
+
const horizontal = dir === "left" || dir === "right"
|
|
379
|
+
// Anchor the track to the top-left corner so its two identical copies tile
|
|
380
|
+
// the container exactly. The track is positioned absolutely, out of the
|
|
381
|
+
// container's flex layout — otherwise a `justify-center` (or any alignment)
|
|
382
|
+
// on the container centers the overflowing track and shifts the tile seam,
|
|
383
|
+
// which opens a gap on the trailing edge at some point in the loop.
|
|
384
|
+
target.style.position = "relative"
|
|
385
|
+
target.style.overflow = "hidden"
|
|
386
|
+
const track = document.createElement("div")
|
|
387
|
+
track.style.cssText = `position:absolute;top:${yOffset}px;left:${xOffset}px;display:flex;flex-direction:${horizontal ? "row" : "column"};width:max-content;will-change:transform;`
|
|
388
|
+
while (target.firstChild) track.appendChild(target.firstChild)
|
|
389
|
+
target.appendChild(track)
|
|
390
|
+
|
|
391
|
+
// The track is absolutely positioned, so once its content moves in, the host
|
|
392
|
+
// has no in-flow children left and can collapse to zero height. With the
|
|
393
|
+
// `overflow:hidden` set above that clips the track away entirely (e.g. a
|
|
394
|
+
// bare-text `<h1 class="marquee-left">` disappears). Preserve the content's
|
|
395
|
+
// height on the host when that happens so the marquee stays visible. Hosts
|
|
396
|
+
// with their own height (flex cards etc.) are left untouched.
|
|
397
|
+
if (target.offsetHeight === 0 && track.offsetHeight > 0) {
|
|
398
|
+
target.style.height = track.offsetHeight + "px"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const first = track.children[0]
|
|
402
|
+
if (!first) return gsap.fromTo(track, {}, { duration: 0 })
|
|
403
|
+
|
|
404
|
+
// A single copy of the content (the width one loop step must travel).
|
|
405
|
+
const unitHtml = track.innerHTML
|
|
406
|
+
const unitSize = horizontal ? track.scrollWidth : track.scrollHeight
|
|
407
|
+
|
|
408
|
+
// Default: repeat the unit until the whole strip is at least as wide as the
|
|
409
|
+
// viewport (plus one extra copy so the trailing edge stays covered mid-loop).
|
|
410
|
+
// `.marquee-no-repeat` opts into the minimal 2-copy single-seam behaviour.
|
|
411
|
+
let copies
|
|
412
|
+
if (noRepeat) {
|
|
413
|
+
copies = 2
|
|
414
|
+
} else {
|
|
415
|
+
const viewport = horizontal ? target.offsetWidth : target.offsetHeight
|
|
416
|
+
copies = Math.max(2, Math.ceil(viewport / unitSize) + 1)
|
|
417
|
+
}
|
|
418
|
+
track.innerHTML = unitHtml.repeat(copies)
|
|
419
|
+
|
|
420
|
+
const dist = unitSize
|
|
421
|
+
const vars = { duration: duration, ease: "none" }
|
|
422
|
+
|
|
423
|
+
if (dir === "right") return gsap.fromTo(track, { x: -dist }, { x: 0, ...vars })
|
|
424
|
+
if (dir === "up") return gsap.fromTo(track, { y: 0 }, { y: -dist, ...vars })
|
|
425
|
+
if (dir === "down") return gsap.fromTo(track, { y: -dist }, { y: 0, ...vars })
|
|
426
|
+
return gsap.fromTo(track, { x: 0 }, { x: -dist, ...vars }) // left
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
//Extras
|
|
430
|
+
export function flip (state , ease , dur){
|
|
431
|
+
Flip.from(state, {duration: dur, ease: ease || DEFAULT_EASE});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function animatecss (target , dur , delay , ease, propertyS , propertySValue , propertyE , propertyEValue) {
|
|
435
|
+
return gsap.fromTo(target , {[propertyS]:propertySValue} , {[propertyE]:propertyEValue , ease:ease, duration:dur, delay:delay})
|
|
436
|
+
}
|