@mehshekari/gsap-motion 2.1.0
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/LICENSE +21 -0
- package/NOTICE.md +24 -0
- package/README.md +59 -0
- package/bin/gsap-motion.mjs +304 -0
- package/package.json +48 -0
- package/skill/LICENSE +21 -0
- package/skill/NOTICE.md +24 -0
- package/skill/SKILL.md +181 -0
- package/skill/example/cinematic-intro.md +202 -0
- package/skill/example/magnetic-button.md +101 -0
- package/skill/example/svg-loader.md +180 -0
- package/skill/preset/card-stack.md +175 -0
- package/skill/preset/cinematic.md +140 -0
- package/skill/preset/cursor.md +184 -0
- package/skill/preset/loader.md +172 -0
- package/skill/preset/magnetic.md +155 -0
- package/skill/preset/marquee.md +177 -0
- package/skill/preset/page-transition.md +151 -0
- package/skill/preset/reveal.md +146 -0
- package/skill/preset/scroll-storytelling.md +176 -0
- package/skill/reference/accessibility.md +143 -0
- package/skill/reference/core-gsap.md +233 -0
- package/skill/reference/flip.md +220 -0
- package/skill/reference/frameworks.md +153 -0
- package/skill/reference/interaction.md +183 -0
- package/skill/reference/motion-design.md +144 -0
- package/skill/reference/performance.md +216 -0
- package/skill/reference/project-rules.md +178 -0
- package/skill/reference/react-nextjs.md +234 -0
- package/skill/reference/routing.md +119 -0
- package/skill/reference/scrolltrigger.md +191 -0
- package/skill/reference/svg.md +201 -0
- package/skill/reference/text.md +150 -0
- package/skill/reference/three-r3f.md +161 -0
- package/skill/reference/timeline.md +147 -0
- package/skill/scripts/audit-gsap.mjs +157 -0
- package/skill/scripts/audit-svg.mjs +358 -0
- package/skill/scripts/lib/rules.mjs +555 -0
- package/skill/scripts/lib/source.mjs +238 -0
- package/skill/template/useScrollScene.ts +126 -0
- package/skill/template/useTimeline.ts +110 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Scroll storytelling
|
|
2
|
+
|
|
3
|
+
A pinned section where scrolling drives a sequence. The "Apple product page"
|
|
4
|
+
pattern. Expensive in build time and in the visitor's attention, so it has to
|
|
5
|
+
be carrying a story that genuinely has an order.
|
|
6
|
+
|
|
7
|
+
Mechanics are in [scrolltrigger.md](../reference/scrolltrigger.md); this is how
|
|
8
|
+
to compose one.
|
|
9
|
+
|
|
10
|
+
## Decide the beats first
|
|
11
|
+
|
|
12
|
+
Write them out before any code. Each beat is one idea, and scroll distance is
|
|
13
|
+
your pacing:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
0.0 – 0.2 the product, whole
|
|
17
|
+
0.2 – 0.45 it opens; the first claim arrives
|
|
18
|
+
0.45 – 0.7 detail view; the second claim
|
|
19
|
+
0.7 – 1.0 it closes; the CTA
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Roughly 600–1000px of scroll per beat. Less and it flicks past; more and the
|
|
23
|
+
visitor thinks the page is stuck.
|
|
24
|
+
|
|
25
|
+
## One timeline, one trigger
|
|
26
|
+
|
|
27
|
+
Every element in the sequence belongs to the same timeline. Separate triggers
|
|
28
|
+
drift apart the moment you retime anything.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const tl = gsap.timeline({
|
|
32
|
+
defaults: { ease: "none" }, // scrubbed — the scrollbar owns the feel
|
|
33
|
+
scrollTrigger: {
|
|
34
|
+
trigger: section,
|
|
35
|
+
start: "top top",
|
|
36
|
+
end: "+=3000",
|
|
37
|
+
pin: true,
|
|
38
|
+
scrub: 1,
|
|
39
|
+
anticipatePin: 1,
|
|
40
|
+
invalidateOnRefresh: true,
|
|
41
|
+
markers: process.env.NODE_ENV === "development",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
tl.addLabel("open")
|
|
46
|
+
.to("[data-lid]", { rotationX: -110, duration: 1 })
|
|
47
|
+
.from("[data-claim-1]", { autoAlpha: 0, y: 20, duration: 0.4 }, "open+=0.4")
|
|
48
|
+
|
|
49
|
+
.addLabel("detail")
|
|
50
|
+
.to("[data-claim-1]", { autoAlpha: 0, duration: 0.3 }, "detail")
|
|
51
|
+
.to("[data-product]", { scale: 1.6, x: "-20%", duration: 1 }, "detail")
|
|
52
|
+
.from("[data-claim-2]", { autoAlpha: 0, y: 20, duration: 0.4 }, "detail+=0.4");
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`ease: "none"` on every child. Easing inside a scrubbed timeline fights the
|
|
56
|
+
visitor's own scrolling and reads as lag — `scrub: 1` is where the smoothing
|
|
57
|
+
belongs.
|
|
58
|
+
|
|
59
|
+
## Horizontal
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const panels = gsap.utils.toArray<HTMLElement>("[data-panel]");
|
|
63
|
+
|
|
64
|
+
gsap.to(panels, {
|
|
65
|
+
xPercent: -100 * (panels.length - 1),
|
|
66
|
+
ease: "none",
|
|
67
|
+
scrollTrigger: {
|
|
68
|
+
trigger: track,
|
|
69
|
+
pin: true,
|
|
70
|
+
scrub: 1,
|
|
71
|
+
end: () => `+=${track.offsetWidth}`,
|
|
72
|
+
snap: { snapTo: 1 / (panels.length - 1), duration: 0.4 },
|
|
73
|
+
invalidateOnRefresh: true,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`end` as a function is re-evaluated on refresh, which keeps it correct after a
|
|
79
|
+
resize. `xPercent` avoids measuring widths.
|
|
80
|
+
|
|
81
|
+
In RTL the direction inverts — `+100 *` rather than `-100 *`. Read `dir` from
|
|
82
|
+
the document; see [project-rules.md](../reference/project-rules.md).
|
|
83
|
+
|
|
84
|
+
Horizontal scroll costs the visitor their scroll intuition. Justify it with
|
|
85
|
+
content that is genuinely a sequence.
|
|
86
|
+
|
|
87
|
+
## Snapping
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
snap: { snapTo: "labels", duration: 0.4, ease: "power2.inOut", delay: 0.1 }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`"labels"` snaps to the timeline's own labels, which is why naming the beats
|
|
94
|
+
pays off twice. Snapping suits discrete steps and fights continuous motion —
|
|
95
|
+
do not snap a scrubbed camera move.
|
|
96
|
+
|
|
97
|
+
## Progress
|
|
98
|
+
|
|
99
|
+
A scrubbed indicator is cheap and orients the visitor inside a long pin:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
.to("[data-progress]", { scaleX: 1, transformOrigin: "0% 50%" }, 0);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Placed at position `0` with the timeline's full duration, it tracks overall
|
|
106
|
+
progress regardless of what else is happening.
|
|
107
|
+
|
|
108
|
+
## Mobile
|
|
109
|
+
|
|
110
|
+
**Do not scale the desktop sequence down.** A 3000px pin on a phone is a long
|
|
111
|
+
time to be stuck, and pinning fights the address bar collapsing.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
mm.add({ desktop: "(min-width: 768px)", mobile: "(max-width: 767px)" }, (c) => {
|
|
115
|
+
if (c.conditions!.mobile) {
|
|
116
|
+
// Each beat becomes its own in-view reveal. Same content, no pin.
|
|
117
|
+
gsap.utils.toArray("[data-beat]").forEach((beat) =>
|
|
118
|
+
gsap.from(beat as HTMLElement, {
|
|
119
|
+
autoAlpha: 0, y: 24,
|
|
120
|
+
scrollTrigger: { trigger: beat as HTMLElement, start: "top 75%" },
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// the pinned sequence
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This is the single most important decision in this preset. A different strategy,
|
|
130
|
+
not a smaller one.
|
|
131
|
+
|
|
132
|
+
## Reduced motion
|
|
133
|
+
|
|
134
|
+
Drop the pin and the scrub. Each beat appears on entry:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
if (reduced) {
|
|
138
|
+
gsap.set("[data-claim-1], [data-claim-2]", { autoAlpha: 1, y: 0 });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Scroll-scrubbed pinning is a textbook vestibular trigger — the content moves
|
|
144
|
+
while the page does not. The content must still be reachable and readable.
|
|
145
|
+
|
|
146
|
+
## Image sequences
|
|
147
|
+
|
|
148
|
+
Frame-by-frame scrubbing looks expensive because it is. Before committing:
|
|
149
|
+
30–120 frames at a sensible resolution, preloaded, drawn to a `<canvas>` — not
|
|
150
|
+
120 `<img>` elements.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const frame = { i: 0 };
|
|
154
|
+
gsap.to(frame, {
|
|
155
|
+
i: images.length - 1,
|
|
156
|
+
snap: "i",
|
|
157
|
+
ease: "none",
|
|
158
|
+
scrollTrigger: { trigger: section, start: "top top", end: "+=3000",
|
|
159
|
+
pin: true, scrub: 1 },
|
|
160
|
+
onUpdate: () => ctx.drawImage(images[frame.i], 0, 0),
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Do not start this until every frame is decoded, or the first scroll shows gaps.
|
|
165
|
+
On a slow connection, offer a static hero instead.
|
|
166
|
+
|
|
167
|
+
## Failures
|
|
168
|
+
|
|
169
|
+
| Symptom | Cause |
|
|
170
|
+
|---|---|
|
|
171
|
+
| Pin does nothing | an ancestor has `overflow: hidden` |
|
|
172
|
+
| Motion lags behind scroll | easing inside a scrubbed timeline |
|
|
173
|
+
| Beats drift after an edit | separate triggers instead of one timeline |
|
|
174
|
+
| Wrong positions after load | images without dimensions — refresh |
|
|
175
|
+
| Unusable on mobile | desktop sequence scaled instead of replaced |
|
|
176
|
+
| Flicker when the pin engages | missing `anticipatePin` |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
Reduced motion is the part people get wrong, and they get it wrong in the same
|
|
4
|
+
way: they treat it as an off switch.
|
|
5
|
+
|
|
6
|
+
## Reduced motion is a design
|
|
7
|
+
|
|
8
|
+
`prefers-reduced-motion: reduce` is set by people who get motion sickness,
|
|
9
|
+
vertigo, or migraines from movement — and by people who simply find it
|
|
10
|
+
distracting. Vestibular triggers are specific: **large travel, parallax,
|
|
11
|
+
zooming, spinning, and anything the visitor did not initiate.** A cross-fade is
|
|
12
|
+
usually fine. A modal scaling from 0.96 to 1 is usually fine.
|
|
13
|
+
|
|
14
|
+
So the question is never "animate or not". It is: *what was this motion saying,
|
|
15
|
+
and how do I say it without the travel?*
|
|
16
|
+
|
|
17
|
+
| Full | Reduced | Why |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Section slides up 60px and fades | fades over 200ms | the arrival still reads |
|
|
20
|
+
| Logo draws itself over 1.2s | appears drawn, holds | the brand moment survives |
|
|
21
|
+
| Loader spins | pulses opacity gently | "still working" is the message |
|
|
22
|
+
| Parallax layers | static, correctly composed | parallax says nothing essential |
|
|
23
|
+
| Scroll-scrubbed sequence | each state on entry, no scrub | the content still arrives |
|
|
24
|
+
| Card lifts and scales on hover | border and shadow change | the affordance is what matters |
|
|
25
|
+
| Page transition wipe | instant swap | it was continuity decoration |
|
|
26
|
+
|
|
27
|
+
**A loader with nothing moving reads as a page that has hung** — which is the
|
|
28
|
+
one thing a loader exists to rule out. Keep a gentle opacity pulse.
|
|
29
|
+
|
|
30
|
+
## Implementing it
|
|
31
|
+
|
|
32
|
+
`gsap.matchMedia` handles the preference *changing mid-session*, which a plain
|
|
33
|
+
check does not:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const mm = gsap.matchMedia();
|
|
37
|
+
|
|
38
|
+
mm.add(
|
|
39
|
+
{
|
|
40
|
+
motion: "(prefers-reduced-motion: no-preference)",
|
|
41
|
+
reduced: "(prefers-reduced-motion: reduce)",
|
|
42
|
+
},
|
|
43
|
+
(context) => {
|
|
44
|
+
const { reduced } = context.conditions as Record<string, boolean>;
|
|
45
|
+
|
|
46
|
+
if (reduced) {
|
|
47
|
+
gsap.set("[data-item]", { autoAlpha: 1, y: 0 });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
gsap.from("[data-item]", { y: 60, autoAlpha: 0, stagger: 0.06 });
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Built inside a `useGSAP` body this needs no explicit teardown — `matchMedia`
|
|
58
|
+
registers with the active context and is reverted with it.
|
|
59
|
+
|
|
60
|
+
Everything built in the losing branch is reverted when the preference flips.
|
|
61
|
+
Without `matchMedia`, a visitor who turns reduced motion on mid-session keeps
|
|
62
|
+
the animation they asked to stop.
|
|
63
|
+
|
|
64
|
+
**The reduced branch must still set the end state.** A `from` tween that never
|
|
65
|
+
runs leaves the element at its authored start — invisible. This is the most
|
|
66
|
+
common reduced-motion bug, and it hides content rather than just skipping
|
|
67
|
+
motion.
|
|
68
|
+
|
|
69
|
+
## Announcement
|
|
70
|
+
|
|
71
|
+
- A loader that blocks content: `role="status"` and an accessible name. It
|
|
72
|
+
announces politely without stealing focus.
|
|
73
|
+
- Decorative artwork: `aria-hidden="true"`, and make sure nothing inside it is
|
|
74
|
+
focusable.
|
|
75
|
+
- **Never both.** An `sr-only` label inside an `aria-hidden` container is never
|
|
76
|
+
read — it is dead markup.
|
|
77
|
+
- An overlay covering the page: the content beneath is still in the accessibility
|
|
78
|
+
tree. For a transient splash that is the right outcome — the visitor reaches
|
|
79
|
+
the content immediately. For a modal it is not, and you need `inert` or
|
|
80
|
+
`aria-modal` with focus management.
|
|
81
|
+
|
|
82
|
+
## Focus
|
|
83
|
+
|
|
84
|
+
- Never destroy the focus ring. A transform that moves an element out from under
|
|
85
|
+
its outline, or an `overflow: hidden` ancestor clipping it mid-animation, both
|
|
86
|
+
count.
|
|
87
|
+
- Hover and focus should produce the same visual state. An effect that only
|
|
88
|
+
exists on `:hover` is invisible to a keyboard.
|
|
89
|
+
- An animation must not move a focused element out of the viewport.
|
|
90
|
+
- If an animation reveals interactive content, it must not be focusable before
|
|
91
|
+
it is visible. `autoAlpha` handles this; plain `opacity` does not.
|
|
92
|
+
|
|
93
|
+
## Timing
|
|
94
|
+
|
|
95
|
+
- Nothing essential should depend on motion. If the animation is the only thing
|
|
96
|
+
that says "saved", that is a failure for anyone who cannot see it.
|
|
97
|
+
- Anything auto-playing for longer than 5 seconds and looping needs a way to
|
|
98
|
+
stop it — WCAG 2.2.2. A marquee of logos qualifies.
|
|
99
|
+
- Do not delay content the visitor came for behind an entrance. An intro that
|
|
100
|
+
cannot be skipped needs a hard time ceiling, and dismissal should be a race
|
|
101
|
+
against that ceiling rather than a sequence that can hang.
|
|
102
|
+
|
|
103
|
+
## Flashing
|
|
104
|
+
|
|
105
|
+
Nothing should flash more than three times per second — WCAG 2.3.1. Rapid
|
|
106
|
+
opacity or colour cycling, scramble effects at high speed, and strobing hover
|
|
107
|
+
states all get close.
|
|
108
|
+
|
|
109
|
+
## Touch
|
|
110
|
+
|
|
111
|
+
- Gate hover effects on `(hover: hover) and (pointer: fine)`. On touch,
|
|
112
|
+
`:hover` sticks after a tap and the state never clears.
|
|
113
|
+
- A cursor follower on a touch device is dead weight sitting in a corner.
|
|
114
|
+
- Anything that only responds to drag needs a keyboard path to the same outcome.
|
|
115
|
+
|
|
116
|
+
## A marker that means "current"
|
|
117
|
+
|
|
118
|
+
A pill, underline or tint whose *position* says which tab, section or page is
|
|
119
|
+
current is a user-interface component, not decoration. WCAG 1.4.11 asks 3:1
|
|
120
|
+
against what it sits on. Motion makes a marker easy to notice while it travels;
|
|
121
|
+
at rest it has to be readable without that.
|
|
122
|
+
|
|
123
|
+
Brand fills rarely manage it on a light ground. `#FFD400` on white is 1.43:1,
|
|
124
|
+
and the soft brand tints are lower still. That does not rule a tint out — it
|
|
125
|
+
rules out the tint being the **only** thing that says "current". Carry the
|
|
126
|
+
state twice more:
|
|
127
|
+
|
|
128
|
+
- **In the label.** The current item keeps a colour that meets text contrast
|
|
129
|
+
even while the marker has moved off to follow the pointer.
|
|
130
|
+
- **In `aria-current`.** `page` for a route, `location` for a section inside
|
|
131
|
+
the page being read. This is the part that survives with no eyes on it.
|
|
132
|
+
|
|
133
|
+
## Checklist
|
|
134
|
+
|
|
135
|
+
- Is there a reduced-motion branch, and does it still communicate?
|
|
136
|
+
- Does the reduced branch set the end state, not just skip the tween?
|
|
137
|
+
- Is `matchMedia` used, so a mid-session change is honoured?
|
|
138
|
+
- Is the artwork either announced or hidden — and not both?
|
|
139
|
+
- Does hover have a focus equivalent?
|
|
140
|
+
- Is the focus ring intact throughout?
|
|
141
|
+
- Can a long loop be stopped?
|
|
142
|
+
- Does anything essential exist only as motion?
|
|
143
|
+
- Does a "current" marker reach 3:1 — or is the state also in the label and `aria-current`?
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Core GSAP
|
|
2
|
+
|
|
3
|
+
The API you actually reach for. Plugin-specific surfaces live in their own
|
|
4
|
+
files; this is what is true everywhere.
|
|
5
|
+
|
|
6
|
+
## Imports
|
|
7
|
+
|
|
8
|
+
Every plugin is in the public package. Import each from its own subpath so the
|
|
9
|
+
bundler can drop the ones a route does not use — never `gsap/all`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import gsap from "gsap";
|
|
13
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
14
|
+
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Register once, at module scope of the file that owns the animation:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
gsap.registerPlugin(useGSAP, ScrollTrigger, DrawSVGPlugin);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Registering `useGSAP` alongside them is what makes GSAP warn about a missing
|
|
24
|
+
plugin instead of silently ignoring the property that needed it.
|
|
25
|
+
|
|
26
|
+
Available here, all free, all typed: `ScrollTrigger`, `ScrollSmoother`,
|
|
27
|
+
`ScrollToPlugin`, `Observer`, `Draggable`, `InertiaPlugin`, `Flip`, `SplitText`,
|
|
28
|
+
`ScrambleTextPlugin`, `TextPlugin`, `DrawSVGPlugin`, `MorphSVGPlugin`,
|
|
29
|
+
`MotionPathPlugin`, `MotionPathHelper`, `CustomEase`, `CustomWiggle`,
|
|
30
|
+
`CustomBounce`, `Physics2DPlugin`, `PhysicsPropsPlugin`, `GSDevTools`,
|
|
31
|
+
`PixiPlugin`, `EaselPlugin`, `CSSRulePlugin`, `EasePack`.
|
|
32
|
+
|
|
33
|
+
## The four verbs
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
gsap.to(target, vars); // from current state to vars
|
|
37
|
+
gsap.from(target, vars); // from vars to current state
|
|
38
|
+
gsap.fromTo(target, fromVars, to); // explicit both ends
|
|
39
|
+
gsap.set(target, vars); // instant, no tween
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`from` is convenient and it is also the usual cause of a flash: the element
|
|
43
|
+
paints at its natural state before the tween applies. `fromTo` with an explicit
|
|
44
|
+
start, or a `gsap.set` before, is what removes that. In React, `useGSAP` runs in
|
|
45
|
+
a layout effect so the set lands before paint — that is most of why it matters.
|
|
46
|
+
|
|
47
|
+
## Targets
|
|
48
|
+
|
|
49
|
+
A selector string, an element, a ref's `.current`, an array, or an object.
|
|
50
|
+
Inside a `useGSAP` or `gsap.context` scope, selector strings resolve against the
|
|
51
|
+
scope rather than the document — which is the whole reason to use one.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
gsap.to("[data-card]", { ... }); // scoped, when inside useGSAP
|
|
55
|
+
gsap.to(ref.current, { ... }); // explicit
|
|
56
|
+
gsap.to({ v: 0 }, { v: 100, onUpdate }); // tween a plain object
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That last form is how you animate anything GSAP cannot touch directly — a
|
|
60
|
+
canvas value, a counter, a Three.js material, a CSS variable on a distant node.
|
|
61
|
+
|
|
62
|
+
## Properties
|
|
63
|
+
|
|
64
|
+
Transforms are first-class and composable, and GSAP writes them as one matrix
|
|
65
|
+
rather than fighting over `transform`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
{ x: 100, y: -20, xPercent: -50, rotation: 45, scale: 1.2, skewX: 8,
|
|
69
|
+
transformOrigin: "50% 100%" }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`xPercent`/`yPercent` are percentages of the element's own size, which is what
|
|
73
|
+
centres something without knowing its dimensions.
|
|
74
|
+
|
|
75
|
+
`autoAlpha` is `opacity` plus `visibility` — it hides the element from pointer
|
|
76
|
+
events at 0, which plain `opacity` does not.
|
|
77
|
+
|
|
78
|
+
CSS variables work, and are the cheapest way to drive something whose styling
|
|
79
|
+
lives in CSS:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
gsap.to(root, { "--glow-strength": 1, duration: 0.6 });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Useful vars
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
{
|
|
89
|
+
duration, delay, ease,
|
|
90
|
+
repeat: -1, // forever
|
|
91
|
+
yoyo: true, // reverse on alternate repeats
|
|
92
|
+
repeatDelay: 0.4,
|
|
93
|
+
stagger: 0.06, // or { each, amount, from, ease }
|
|
94
|
+
overwrite: "auto", // kill conflicting tweens on the same props
|
|
95
|
+
paused: true,
|
|
96
|
+
onComplete, onUpdate, onStart,
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`overwrite: "auto"` is the fix for the classic hover bug where leaving and
|
|
101
|
+
re-entering fast leaves an element halfway. Use it on anything an event can
|
|
102
|
+
retrigger.
|
|
103
|
+
|
|
104
|
+
## `gsap.utils`
|
|
105
|
+
|
|
106
|
+
Small functions that remove most of the arithmetic from animation code.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
gsap.utils.clamp(0, 1, v);
|
|
110
|
+
gsap.utils.mapRange(0, 500, 0, 1, scrollY); // remap one range onto another
|
|
111
|
+
gsap.utils.interpolate("#000", "#fff", 0.5);
|
|
112
|
+
gsap.utils.wrap(0, 10, 13); // → 3, for infinite loops
|
|
113
|
+
gsap.utils.snap(0.1, 0.34); // → 0.3
|
|
114
|
+
gsap.utils.random(-20, 20, 1);
|
|
115
|
+
gsap.utils.toArray("[data-card]"); // NodeList → real array
|
|
116
|
+
gsap.utils.selector(ref); // scoped selector function
|
|
117
|
+
gsap.utils.shuffle(items);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`mapRange` and `wrap` are the two that most often replace ten lines of
|
|
121
|
+
hand-rolled maths in a scroll or marquee.
|
|
122
|
+
|
|
123
|
+
## `quickTo` and `quickSetter`
|
|
124
|
+
|
|
125
|
+
For values driven by a high-frequency event — pointer move, scroll, rAF —
|
|
126
|
+
creating a tween per event allocates garbage and fights itself.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const moveX = gsap.quickTo(el, "x", { duration: 0.5, ease: "power3" });
|
|
130
|
+
const moveY = gsap.quickTo(el, "y", { duration: 0.5, ease: "power3" });
|
|
131
|
+
|
|
132
|
+
// In the handler — reuses one tween, no allocation:
|
|
133
|
+
moveX(e.clientX);
|
|
134
|
+
moveY(e.clientY);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`quickSetter` is the same idea with no interpolation, for when you are already
|
|
138
|
+
smoothing the value yourself:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const setX = gsap.quickSetter(el, "x", "px");
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This is the single most important performance tool for pointer work. See
|
|
145
|
+
[interaction.md](interaction.md).
|
|
146
|
+
|
|
147
|
+
## `matchMedia`
|
|
148
|
+
|
|
149
|
+
Different strategies per breakpoint or preference, with automatic teardown when
|
|
150
|
+
the condition stops matching.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const mm = gsap.matchMedia();
|
|
154
|
+
|
|
155
|
+
mm.add(
|
|
156
|
+
{
|
|
157
|
+
desktop: "(min-width: 768px)",
|
|
158
|
+
mobile: "(max-width: 767px)",
|
|
159
|
+
reduced: "(prefers-reduced-motion: reduce)",
|
|
160
|
+
},
|
|
161
|
+
(context) => {
|
|
162
|
+
const { desktop, reduced } = context.conditions as Record<string, boolean>;
|
|
163
|
+
if (reduced) { /* the reduced design */ return; }
|
|
164
|
+
gsap.to(el, { x: desktop ? 200 : 60 });
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Teardown, when it was not created inside a context:
|
|
169
|
+
mm.revert();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Everything created inside the callback is reverted when the condition stops
|
|
173
|
+
matching. A plain `window.matchMedia` check leaves the first animation running
|
|
174
|
+
forever when a visitor turns reduced motion on mid-session, or rotates a tablet.
|
|
175
|
+
|
|
176
|
+
**Created inside a `useGSAP` or `gsap.context` body, it reverts itself with
|
|
177
|
+
that context** — `MatchMedia`'s constructor registers with whatever context is
|
|
178
|
+
active (verified against gsap 3.15), and `Context.add` makes itself active while your
|
|
179
|
+
function runs. An explicit `mm.revert()` there is redundant, not wrong. Created
|
|
180
|
+
outside one — module scope, a plain `useEffect` — it is yours to revert. See
|
|
181
|
+
[react-nextjs.md](react-nextjs.md).
|
|
182
|
+
|
|
183
|
+
## `context`
|
|
184
|
+
|
|
185
|
+
Scopes selectors and collects everything for teardown. In React you get this
|
|
186
|
+
through `useGSAP` and rarely call it directly.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const ctx = gsap.context(() => {
|
|
190
|
+
gsap.to(".card", { y: 0 }); // resolved inside `root`
|
|
191
|
+
}, root);
|
|
192
|
+
|
|
193
|
+
ctx.revert(); // undoes everything, restores inline styles
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`revert()` restores original inline styles; `kill()` only stops the tweens. For
|
|
197
|
+
a component unmount you almost always want `revert`.
|
|
198
|
+
|
|
199
|
+
**A scope does not reach inside plugin config.** `motionPath.path` and
|
|
200
|
+
`morphSVG.shape` are resolved by the plugin against the whole document, so a
|
|
201
|
+
fixed id in a component rendered twice makes both instances drive the first
|
|
202
|
+
one's geometry — with no error. Namespace those ids per instance.
|
|
203
|
+
|
|
204
|
+
## Control
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
tl.play(); tl.pause(); tl.reverse(); tl.restart();
|
|
208
|
+
tl.progress(0.5); // jump
|
|
209
|
+
tl.timeScale(2); // twice as fast, live
|
|
210
|
+
tl.seek("label");
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Tweening a timeline's own progress is how you smooth a jumpy external value:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
gsap.to(tl, { progress: uploaded, duration: 0.4, overwrite: true });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Easing
|
|
220
|
+
|
|
221
|
+
Named: `power1`–`power4`, `back`, `elastic`, `bounce`, `circ`, `expo`, `sine`,
|
|
222
|
+
`steps(n)`, `none`, each with `.in`/`.out`/`.inOut`. Which to pick is a craft
|
|
223
|
+
question — see [motion-design.md](motion-design.md).
|
|
224
|
+
|
|
225
|
+
`CustomEase` takes an SVG path and is free:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
CustomEase.create("brand", "M0,0 C0.16,1 0.3,1 1,1");
|
|
229
|
+
gsap.to(el, { y: 0, ease: "brand" });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`CustomWiggle` and `CustomBounce` register named eases the same way, for shake
|
|
233
|
+
and impact without hand-built keyframes.
|