@mehshekari/gsap-motion 3.1.0 → 3.2.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/README.md +4 -1
- package/bin/gsap-motion.mjs +17 -0
- package/package.json +1 -1
- package/skill/SKILL.md +14 -10
- package/skill/adapter/astro.md +111 -0
- package/skill/adapter/next.md +99 -0
- package/skill/adapter/r3f.md +128 -0
- package/skill/adapter/react.md +133 -0
- package/skill/adapter/svelte.md +105 -0
- package/skill/adapter/three.md +141 -0
- package/skill/adapter/vanilla.md +115 -0
- package/skill/adapter/vue.md +120 -0
- package/skill/example/svg-loader.md +1 -1
- package/skill/reference/core-gsap.md +1 -1
- package/skill/reference/interaction.md +183 -183
- package/skill/reference/routing.md +119 -119
- package/skill/reference/scrolltrigger.md +1 -1
- package/skill/reference/svg.md +1 -1
- package/skill/reference/timeline.md +1 -1
- package/skill/scripts/audit-gsap.mjs +21 -59
- package/skill/scripts/lib/ast.mjs +7 -1
- package/skill/scripts/lib/audit.mjs +91 -0
- package/skill/scripts/lib/rules.mjs +129 -3
- package/skill/scripts/lib/source.mjs +79 -8
- package/skill/reference/frameworks.md +0 -153
- package/skill/reference/react-nextjs.md +0 -234
- package/skill/reference/three-r3f.md +0 -161
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
# Interaction
|
|
2
|
-
|
|
3
|
-
Pointer-driven motion. The engineering constraint is that these fire at 60–120Hz,
|
|
4
|
-
so the wrong architecture costs a frame budget rather than a few milliseconds.
|
|
5
|
-
|
|
6
|
-
Every interaction has three parts, and the third is the one people skip:
|
|
7
|
-
|
|
8
|
-
```text
|
|
9
|
-
input → response → recovery
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
The recovery is what makes it feel physical. A button that springs toward the
|
|
13
|
-
cursor and snaps back on leave feels broken; the same button easing back over
|
|
14
|
-
0.6s feels alive.
|
|
15
|
-
|
|
16
|
-
## The rule: `quickTo`, not a tween per event
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
// Wrong — allocates a tween per pointer event, and they fight each other
|
|
20
|
-
onMouseMove = (e) => gsap.to(el, { x: e.clientX, duration: 0.5 });
|
|
21
|
-
|
|
22
|
-
// Right — one tween, retargeted
|
|
23
|
-
const moveX = gsap.quickTo(el, "x", { duration: 0.5, ease: "power3" });
|
|
24
|
-
const moveY = gsap.quickTo(el, "y", { duration: 0.5, ease: "power3" });
|
|
25
|
-
onMouseMove = (e) => { moveX(e.clientX); moveY(e.clientY); };
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Create the `quickTo` functions once, inside the `useGSAP` body. This is the
|
|
29
|
-
single most important performance decision in pointer work.
|
|
30
|
-
|
|
31
|
-
## Magnetic element
|
|
32
|
-
|
|
33
|
-
The element leans toward the cursor while it is near, and returns when it
|
|
34
|
-
leaves.
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
const xTo = gsap.quickTo(el, "x", { duration: 0.6, ease: "power3" });
|
|
38
|
-
const yTo = gsap.quickTo(el, "y", { duration: 0.6, ease: "power3" });
|
|
39
|
-
|
|
40
|
-
const onMove = (e: PointerEvent) => {
|
|
41
|
-
const r = el.getBoundingClientRect();
|
|
42
|
-
xTo((e.clientX - (r.left + r.width / 2)) * 0.35);
|
|
43
|
-
yTo((e.clientY - (r.top + r.height / 2)) * 0.35);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const onLeave = () => { xTo(0); yTo(0); };
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
- **Strength 0.2–0.4.** Above that the element outruns the cursor and feels
|
|
50
|
-
unmoored.
|
|
51
|
-
- **Measure in the handler, not once.** Scroll and layout change the rect. If
|
|
52
|
-
that shows up as cost, cache it on enter and invalidate on scroll.
|
|
53
|
-
- **The hit area should be larger than the element** — a padded wrapper listens,
|
|
54
|
-
the inner element moves. That is what makes the pull feel like a field rather
|
|
55
|
-
than a hover state.
|
|
56
|
-
|
|
57
|
-
## Cursor follower
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
const xTo = gsap.quickTo(dot, "x", { duration: 0.35, ease: "power3" });
|
|
61
|
-
const yTo = gsap.quickTo(dot, "y", { duration: 0.35, ease: "power3" });
|
|
62
|
-
|
|
63
|
-
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
- Two layers at different durations (0.1 and 0.5) give the trailing-ring look
|
|
67
|
-
without a trail of elements.
|
|
68
|
-
- `pointer-events: none` on the follower, always.
|
|
69
|
-
- **Only on devices that have a cursor.** Gate on
|
|
70
|
-
`(hover: hover) and (pointer: fine)` — a follower on a touch device is dead
|
|
71
|
-
weight and sits in the corner.
|
|
72
|
-
- Hiding the native cursor is a real accessibility cost. Do it only if the
|
|
73
|
-
replacement is at least as legible over every surface, and never over text
|
|
74
|
-
inputs.
|
|
75
|
-
|
|
76
|
-
## Observer
|
|
77
|
-
|
|
78
|
-
One unified handler for wheel, touch, and pointer — the right tool when you want
|
|
79
|
-
a gesture rather than a scroll position.
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
import { Observer } from "gsap/Observer";
|
|
83
|
-
gsap.registerPlugin(Observer);
|
|
84
|
-
|
|
85
|
-
const obs = Observer.create({
|
|
86
|
-
target: window,
|
|
87
|
-
type: "wheel,touch,pointer",
|
|
88
|
-
onUp: () => next(),
|
|
89
|
-
onDown: () => prev(),
|
|
90
|
-
tolerance: 10,
|
|
91
|
-
preventDefault: true,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
obs.kill(); // only needed outside a useGSAP body — see react
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
This is how full-screen section-snapping and swipe carousels are built without
|
|
98
|
-
ScrollTrigger. `preventDefault: true` takes over native scrolling — a large
|
|
99
|
-
commitment, and it must not trap a keyboard or screen-reader user.
|
|
100
|
-
|
|
101
|
-
## Draggable and Inertia
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { Draggable } from "gsap/Draggable";
|
|
105
|
-
import { InertiaPlugin } from "gsap/InertiaPlugin";
|
|
106
|
-
gsap.registerPlugin(Draggable, InertiaPlugin);
|
|
107
|
-
|
|
108
|
-
Draggable.create(el, {
|
|
109
|
-
type: "x",
|
|
110
|
-
bounds: container,
|
|
111
|
-
inertia: true,
|
|
112
|
-
snap: { x: (v) => Math.round(v / 320) * 320 },
|
|
113
|
-
onDragEnd() { /* ... */ },
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
`inertia: true` gives the flick-and-settle that makes a drag feel native.
|
|
118
|
-
`Draggable.create` returns instances — `.kill()` them on unmount.
|
|
119
|
-
|
|
120
|
-
Anything draggable needs a keyboard path to the same outcome. A carousel that
|
|
121
|
-
only responds to drag is unusable without a pointer.
|
|
122
|
-
|
|
123
|
-
## Hover with a timeline
|
|
124
|
-
|
|
125
|
-
Build once, play and reverse. Better than creating tweens per event, and the
|
|
126
|
-
return matches the arrival for free.
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
const hover = gsap.timeline({ paused: true })
|
|
130
|
-
.to(el, { scale: 1.04, duration: 0.3, ease: "power2.out" })
|
|
131
|
-
.to(label, { y: -2, duration: 0.3 }, "<");
|
|
132
|
-
|
|
133
|
-
onEnter = () => hover.play();
|
|
134
|
-
onLeave = () => hover.reverse();
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
In React, if the timeline is built inside `useGSAP` and only *played* from the
|
|
138
|
-
handler, no `contextSafe` is needed — see [react
|
|
139
|
-
|
|
140
|
-
## `overwrite: "auto"`
|
|
141
|
-
|
|
142
|
-
The classic bug: enter, leave, enter fast, and the element sits halfway.
|
|
143
|
-
Whatever retriggers needs `overwrite: "auto"` so the conflicting tween is killed
|
|
144
|
-
rather than blended.
|
|
145
|
-
|
|
146
|
-
## Spotlight
|
|
147
|
-
|
|
148
|
-
A pointer-tracked gradient. Drive CSS variables rather than re-rendering:
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
const setX = gsap.quickSetter(card, "--spot-x", "px");
|
|
152
|
-
const setY = gsap.quickSetter(card, "--spot-y", "px");
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
```css
|
|
156
|
-
background: radial-gradient(
|
|
157
|
-
240px at var(--spot-x) var(--spot-y),
|
|
158
|
-
var(--glow), transparent 70%
|
|
159
|
-
);
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Cheap, and the styling stays in CSS where it can follow the theme.
|
|
163
|
-
|
|
164
|
-
## Accessibility
|
|
165
|
-
|
|
166
|
-
- **Everything reachable by pointer must be reachable by keyboard.** Hover state
|
|
167
|
-
and focus state should be the same visual.
|
|
168
|
-
- Never destroy the focus ring with a transform that moves the element out from
|
|
169
|
-
under it.
|
|
170
|
-
- Gate hover effects on `(hover: hover)`. On touch, `:hover` sticks after a tap.
|
|
171
|
-
- Under `prefers-reduced-motion: reduce`, keep the state change and drop the
|
|
172
|
-
travel — see [accessibility.md](accessibility.md).
|
|
173
|
-
|
|
174
|
-
## Failures
|
|
175
|
-
|
|
176
|
-
| Symptom | Cause |
|
|
177
|
-
|---|---|
|
|
178
|
-
| Janky on move | a tween per event, or React state in the handler |
|
|
179
|
-
| Element sticks halfway | missing `overwrite: "auto"` |
|
|
180
|
-
| Magnetic pull feels wrong | strength too high, or rect measured once |
|
|
181
|
-
| Follower stuck in a corner | mounted on a touch device |
|
|
182
|
-
| Hover sticks after tap | no `(hover: hover)` gate |
|
|
183
|
-
| Effects survive navigation | `Observer`/`Draggable` not killed, listeners not removed |
|
|
1
|
+
# Interaction
|
|
2
|
+
|
|
3
|
+
Pointer-driven motion. The engineering constraint is that these fire at 60–120Hz,
|
|
4
|
+
so the wrong architecture costs a frame budget rather than a few milliseconds.
|
|
5
|
+
|
|
6
|
+
Every interaction has three parts, and the third is the one people skip:
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
input → response → recovery
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The recovery is what makes it feel physical. A button that springs toward the
|
|
13
|
+
cursor and snaps back on leave feels broken; the same button easing back over
|
|
14
|
+
0.6s feels alive.
|
|
15
|
+
|
|
16
|
+
## The rule: `quickTo`, not a tween per event
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// Wrong — allocates a tween per pointer event, and they fight each other
|
|
20
|
+
onMouseMove = (e) => gsap.to(el, { x: e.clientX, duration: 0.5 });
|
|
21
|
+
|
|
22
|
+
// Right — one tween, retargeted
|
|
23
|
+
const moveX = gsap.quickTo(el, "x", { duration: 0.5, ease: "power3" });
|
|
24
|
+
const moveY = gsap.quickTo(el, "y", { duration: 0.5, ease: "power3" });
|
|
25
|
+
onMouseMove = (e) => { moveX(e.clientX); moveY(e.clientY); };
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Create the `quickTo` functions once, inside the `useGSAP` body. This is the
|
|
29
|
+
single most important performance decision in pointer work.
|
|
30
|
+
|
|
31
|
+
## Magnetic element
|
|
32
|
+
|
|
33
|
+
The element leans toward the cursor while it is near, and returns when it
|
|
34
|
+
leaves.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const xTo = gsap.quickTo(el, "x", { duration: 0.6, ease: "power3" });
|
|
38
|
+
const yTo = gsap.quickTo(el, "y", { duration: 0.6, ease: "power3" });
|
|
39
|
+
|
|
40
|
+
const onMove = (e: PointerEvent) => {
|
|
41
|
+
const r = el.getBoundingClientRect();
|
|
42
|
+
xTo((e.clientX - (r.left + r.width / 2)) * 0.35);
|
|
43
|
+
yTo((e.clientY - (r.top + r.height / 2)) * 0.35);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onLeave = () => { xTo(0); yTo(0); };
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Strength 0.2–0.4.** Above that the element outruns the cursor and feels
|
|
50
|
+
unmoored.
|
|
51
|
+
- **Measure in the handler, not once.** Scroll and layout change the rect. If
|
|
52
|
+
that shows up as cost, cache it on enter and invalidate on scroll.
|
|
53
|
+
- **The hit area should be larger than the element** — a padded wrapper listens,
|
|
54
|
+
the inner element moves. That is what makes the pull feel like a field rather
|
|
55
|
+
than a hover state.
|
|
56
|
+
|
|
57
|
+
## Cursor follower
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const xTo = gsap.quickTo(dot, "x", { duration: 0.35, ease: "power3" });
|
|
61
|
+
const yTo = gsap.quickTo(dot, "y", { duration: 0.35, ease: "power3" });
|
|
62
|
+
|
|
63
|
+
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- Two layers at different durations (0.1 and 0.5) give the trailing-ring look
|
|
67
|
+
without a trail of elements.
|
|
68
|
+
- `pointer-events: none` on the follower, always.
|
|
69
|
+
- **Only on devices that have a cursor.** Gate on
|
|
70
|
+
`(hover: hover) and (pointer: fine)` — a follower on a touch device is dead
|
|
71
|
+
weight and sits in the corner.
|
|
72
|
+
- Hiding the native cursor is a real accessibility cost. Do it only if the
|
|
73
|
+
replacement is at least as legible over every surface, and never over text
|
|
74
|
+
inputs.
|
|
75
|
+
|
|
76
|
+
## Observer
|
|
77
|
+
|
|
78
|
+
One unified handler for wheel, touch, and pointer — the right tool when you want
|
|
79
|
+
a gesture rather than a scroll position.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Observer } from "gsap/Observer";
|
|
83
|
+
gsap.registerPlugin(Observer);
|
|
84
|
+
|
|
85
|
+
const obs = Observer.create({
|
|
86
|
+
target: window,
|
|
87
|
+
type: "wheel,touch,pointer",
|
|
88
|
+
onUp: () => next(),
|
|
89
|
+
onDown: () => prev(),
|
|
90
|
+
tolerance: 10,
|
|
91
|
+
preventDefault: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
obs.kill(); // only needed outside a useGSAP body — see adapter/react.md
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is how full-screen section-snapping and swipe carousels are built without
|
|
98
|
+
ScrollTrigger. `preventDefault: true` takes over native scrolling — a large
|
|
99
|
+
commitment, and it must not trap a keyboard or screen-reader user.
|
|
100
|
+
|
|
101
|
+
## Draggable and Inertia
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { Draggable } from "gsap/Draggable";
|
|
105
|
+
import { InertiaPlugin } from "gsap/InertiaPlugin";
|
|
106
|
+
gsap.registerPlugin(Draggable, InertiaPlugin);
|
|
107
|
+
|
|
108
|
+
Draggable.create(el, {
|
|
109
|
+
type: "x",
|
|
110
|
+
bounds: container,
|
|
111
|
+
inertia: true,
|
|
112
|
+
snap: { x: (v) => Math.round(v / 320) * 320 },
|
|
113
|
+
onDragEnd() { /* ... */ },
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`inertia: true` gives the flick-and-settle that makes a drag feel native.
|
|
118
|
+
`Draggable.create` returns instances — `.kill()` them on unmount.
|
|
119
|
+
|
|
120
|
+
Anything draggable needs a keyboard path to the same outcome. A carousel that
|
|
121
|
+
only responds to drag is unusable without a pointer.
|
|
122
|
+
|
|
123
|
+
## Hover with a timeline
|
|
124
|
+
|
|
125
|
+
Build once, play and reverse. Better than creating tweens per event, and the
|
|
126
|
+
return matches the arrival for free.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const hover = gsap.timeline({ paused: true })
|
|
130
|
+
.to(el, { scale: 1.04, duration: 0.3, ease: "power2.out" })
|
|
131
|
+
.to(label, { y: -2, duration: 0.3 }, "<");
|
|
132
|
+
|
|
133
|
+
onEnter = () => hover.play();
|
|
134
|
+
onLeave = () => hover.reverse();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
In React, if the timeline is built inside `useGSAP` and only *played* from the
|
|
138
|
+
handler, no `contextSafe` is needed — see [react.md](../adapter/react.md).
|
|
139
|
+
|
|
140
|
+
## `overwrite: "auto"`
|
|
141
|
+
|
|
142
|
+
The classic bug: enter, leave, enter fast, and the element sits halfway.
|
|
143
|
+
Whatever retriggers needs `overwrite: "auto"` so the conflicting tween is killed
|
|
144
|
+
rather than blended.
|
|
145
|
+
|
|
146
|
+
## Spotlight
|
|
147
|
+
|
|
148
|
+
A pointer-tracked gradient. Drive CSS variables rather than re-rendering:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const setX = gsap.quickSetter(card, "--spot-x", "px");
|
|
152
|
+
const setY = gsap.quickSetter(card, "--spot-y", "px");
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```css
|
|
156
|
+
background: radial-gradient(
|
|
157
|
+
240px at var(--spot-x) var(--spot-y),
|
|
158
|
+
var(--glow), transparent 70%
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Cheap, and the styling stays in CSS where it can follow the theme.
|
|
163
|
+
|
|
164
|
+
## Accessibility
|
|
165
|
+
|
|
166
|
+
- **Everything reachable by pointer must be reachable by keyboard.** Hover state
|
|
167
|
+
and focus state should be the same visual.
|
|
168
|
+
- Never destroy the focus ring with a transform that moves the element out from
|
|
169
|
+
under it.
|
|
170
|
+
- Gate hover effects on `(hover: hover)`. On touch, `:hover` sticks after a tap.
|
|
171
|
+
- Under `prefers-reduced-motion: reduce`, keep the state change and drop the
|
|
172
|
+
travel — see [accessibility.md](accessibility.md).
|
|
173
|
+
|
|
174
|
+
## Failures
|
|
175
|
+
|
|
176
|
+
| Symptom | Cause |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Janky on move | a tween per event, or React state in the handler |
|
|
179
|
+
| Element sticks halfway | missing `overwrite: "auto"` |
|
|
180
|
+
| Magnetic pull feels wrong | strength too high, or rect measured once |
|
|
181
|
+
| Follower stuck in a corner | mounted on a touch device |
|
|
182
|
+
| Hover sticks after tap | no `(hover: hover)` gate |
|
|
183
|
+
| Effects survive navigation | `Observer`/`Draggable` not killed, listeners not removed |
|
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
# Routing
|
|
2
|
-
|
|
3
|
-
What to load, and what to do when the request is a feeling rather than a
|
|
4
|
-
command.
|
|
5
|
-
|
|
6
|
-
## No argument
|
|
7
|
-
|
|
8
|
-
Do not guess a target and do not invent an animation. Ask what should move, and
|
|
9
|
-
offer the menu below — grouped, so the question is easy to answer.
|
|
10
|
-
|
|
11
|
-
> What are we animating?
|
|
12
|
-
>
|
|
13
|
-
> - **Entrance** — something appearing: a hero, a section, a list, an image
|
|
14
|
-
> - **Scroll** — something driven by scroll position: a sequence, a pin, a
|
|
15
|
-
> horizontal run
|
|
16
|
-
> - **Interaction** — something answering the pointer: a button, a cursor, a
|
|
17
|
-
> card
|
|
18
|
-
> - **Continuous** — something always moving: a marquee, an orbit, ambient drift
|
|
19
|
-
> - **Transition** — moving between two states: a route, a modal, a layout
|
|
20
|
-
> - **Loading** — a loader, a progress indicator, a site intro
|
|
21
|
-
> - **Existing** — something already animated that feels wrong, costs too much,
|
|
22
|
-
> or should go
|
|
23
|
-
|
|
24
|
-
## Classifying a description
|
|
25
|
-
|
|
26
|
-
Most requests are a feeling, not a command. Classify, then route.
|
|
27
|
-
|
|
28
|
-
| What they say | What they mean | Route to |
|
|
29
|
-
|---|---|---|
|
|
30
|
-
| "make it cinematic" | slow, weighted, sequenced, one thing at a time | `intro` · [cinematic](../preset/cinematic.md) |
|
|
31
|
-
| "make it feel premium" | fewer moving parts, longer eases, no bounce | `tune` · [motion-design](motion-design.md) |
|
|
32
|
-
| "make it feel alive" | ambient continuous motion under static content | `ambient` · [marquee](../preset/marquee.md) |
|
|
33
|
-
| "make it pop / bolder" | more contrast in timing, not more elements moving | `tune` · [motion-design](motion-design.md) |
|
|
34
|
-
| "like Apple" | scroll-scrubbed, pinned, one idea per screen | `scroll` · [scroll-storytelling](../preset/scroll-storytelling.md) |
|
|
35
|
-
| "futuristic / technical" | scramble, draw-on, mechanical easing, grid reveals | `text` + `svg` |
|
|
36
|
-
| "liquid / organic" | morph, path motion, overlapping soft easing | `svg` · [loader](../preset/loader.md) |
|
|
37
|
-
| "something crazy" | layered techniques with one clear concept | see below |
|
|
38
|
-
| "smooth image reveal" | clip-path or mask, not opacity | [reveal](../preset/reveal.md) |
|
|
39
|
-
| "the hero is boring" | hierarchy problem before a motion problem | `tune`, then `reveal` |
|
|
40
|
-
| "it feels janky" | cost or trigger problem | `audit` · [performance](performance.md) |
|
|
41
|
-
|
|
42
|
-
### "Something crazy"
|
|
43
|
-
|
|
44
|
-
A fade-and-scale is a failure of nerve; twelve techniques at once is a failure
|
|
45
|
-
of taste. Pick **one concept** and let three techniques serve it. The concept is
|
|
46
|
-
the thing you can say in a sentence — "the logo writes itself, then the dot that
|
|
47
|
-
drew it runs off and writes the word" — and every technique either serves that
|
|
48
|
-
sentence or is cut.
|
|
49
|
-
|
|
50
|
-
## Load map
|
|
51
|
-
|
|
52
|
-
The project's own rules come first, then the size tiers in SKILL.md, setup step
|
|
53
|
-
3: a one-element change loads [motion-design](motion-design.md) and the
|
|
54
|
-
command's own reference only. The table applies from component size up — a
|
|
55
|
-
single hover takes [interaction](interaction.md) and nothing else from its row.
|
|
56
|
-
|
|
57
|
-
| Request | Add |
|
|
58
|
-
|---|---|
|
|
59
|
-
| SVG draw / morph / path | [svg](svg.md), [timeline](timeline.md) |
|
|
60
|
-
| Scroll sequence, pin, scrub | [scrolltrigger](scrolltrigger.md), [timeline](timeline.md), [performance](performance.md) |
|
|
61
|
-
| Horizontal scroll | [scrolltrigger](scrolltrigger.md), [scroll-storytelling](../preset/scroll-storytelling.md) |
|
|
62
|
-
| Text reveal, kinetic type | [text](text.md), [timeline](timeline.md) |
|
|
63
|
-
| Magnetic, cursor, hover | [interaction](interaction.md), [performance](performance.md) |
|
|
64
|
-
| Drag, flick, inertia | [interaction](interaction.md) |
|
|
65
|
-
| Layout / shared element / modal | [flip](flip.md) |
|
|
66
|
-
| Page or route transition | [flip](flip.md), [page-transition](../preset/page-transition.md),
|
|
67
|
-
| Loader, splash, intro | [svg](svg.md), [timeline](timeline.md), [accessibility](accessibility.md) |
|
|
68
|
-
| Marquee, orbit, ambient | [timeline](timeline.md), [performance](performance.md) |
|
|
69
|
-
| Three.js / R3F | [three
|
|
70
|
-
| Audit an existing animation | [performance](performance.md), [accessibility](accessibility.md),
|
|
71
|
-
| Retime / re-ease | [motion-design](motion-design.md) only |
|
|
72
|
-
|
|
73
|
-
Do not load a domain file "for completeness". Loading `three
|
|
74
|
-
button hover is the failure this map exists to prevent.
|
|
75
|
-
|
|
76
|
-
## `audit`
|
|
77
|
-
|
|
78
|
-
**Run the checker first.** It is deterministic, it costs a second, and it finds
|
|
79
|
-
the mechanical failures before you spend attention on them:
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
node .claude/skills/gsap-creative-animation/scripts/audit-gsap.mjs <path>
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**When an animation is not visible, ask whether it was ever built.** Before
|
|
86
|
-
touching timings or transforms, check that the code which creates it actually
|
|
87
|
-
runs. Animation is routinely built behind a condition — an image decode race, a
|
|
88
|
-
`ready.then()`, a ScrollTrigger that has not fired, a `matchMedia` branch that
|
|
89
|
-
does not match, an early `return` on a missing ref — and a tween that was never
|
|
90
|
-
created looks exactly like one that is mis-timed. A typical case: an image wipe
|
|
91
|
-
added only once the image decodes inside a short budget. On a first visit the
|
|
92
|
-
image is never cached, the branch never runs, and every hypothesis about the
|
|
93
|
-
wipe's geometry is wasted effort.
|
|
94
|
-
|
|
95
|
-
Then read for what it cannot see. Check in this order — the first two are where
|
|
96
|
-
real bugs live:
|
|
97
|
-
|
|
98
|
-
1. **Lifecycle** — is every animation inside a `useGSAP`/`gsap.context` scope?
|
|
99
|
-
Are ScrollTriggers killed? Are listeners removed? Does a `matchMedia` get
|
|
100
|
-
reverted? See
|
|
101
|
-
2. **Cost** — layout-triggering properties, per-frame React state, filters on
|
|
102
|
-
large areas, one ScrollTrigger per item where one would do. See
|
|
103
|
-
[performance](performance.md).
|
|
104
|
-
3. **Reduced motion** — is there a branch, and does it still communicate?
|
|
105
|
-
4. **Craft** — rhythm, easing on loops, contrast. See [motion-design](motion-design.md).
|
|
106
|
-
|
|
107
|
-
Report findings most-severe first, with the file and line. Do not rewrite
|
|
108
|
-
unless asked.
|
|
109
|
-
|
|
110
|
-
## `strip`
|
|
111
|
-
|
|
112
|
-
Removing motion is a legitimate outcome and often the right one. Cut an
|
|
113
|
-
animation when it answers none of hierarchy, causality, continuity, state, or
|
|
114
|
-
personality; when it delays content the visitor came for; when it repeats on
|
|
115
|
-
every route and stopped being noticed on the second one; or when it exists
|
|
116
|
-
because a plugin was available.
|
|
117
|
-
|
|
118
|
-
Say what you removed and what it was costing. Do not replace it with a smaller
|
|
119
|
-
animation unless the user asks.
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
What to load, and what to do when the request is a feeling rather than a
|
|
4
|
+
command.
|
|
5
|
+
|
|
6
|
+
## No argument
|
|
7
|
+
|
|
8
|
+
Do not guess a target and do not invent an animation. Ask what should move, and
|
|
9
|
+
offer the menu below — grouped, so the question is easy to answer.
|
|
10
|
+
|
|
11
|
+
> What are we animating?
|
|
12
|
+
>
|
|
13
|
+
> - **Entrance** — something appearing: a hero, a section, a list, an image
|
|
14
|
+
> - **Scroll** — something driven by scroll position: a sequence, a pin, a
|
|
15
|
+
> horizontal run
|
|
16
|
+
> - **Interaction** — something answering the pointer: a button, a cursor, a
|
|
17
|
+
> card
|
|
18
|
+
> - **Continuous** — something always moving: a marquee, an orbit, ambient drift
|
|
19
|
+
> - **Transition** — moving between two states: a route, a modal, a layout
|
|
20
|
+
> - **Loading** — a loader, a progress indicator, a site intro
|
|
21
|
+
> - **Existing** — something already animated that feels wrong, costs too much,
|
|
22
|
+
> or should go
|
|
23
|
+
|
|
24
|
+
## Classifying a description
|
|
25
|
+
|
|
26
|
+
Most requests are a feeling, not a command. Classify, then route.
|
|
27
|
+
|
|
28
|
+
| What they say | What they mean | Route to |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| "make it cinematic" | slow, weighted, sequenced, one thing at a time | `intro` · [cinematic](../preset/cinematic.md) |
|
|
31
|
+
| "make it feel premium" | fewer moving parts, longer eases, no bounce | `tune` · [motion-design](motion-design.md) |
|
|
32
|
+
| "make it feel alive" | ambient continuous motion under static content | `ambient` · [marquee](../preset/marquee.md) |
|
|
33
|
+
| "make it pop / bolder" | more contrast in timing, not more elements moving | `tune` · [motion-design](motion-design.md) |
|
|
34
|
+
| "like Apple" | scroll-scrubbed, pinned, one idea per screen | `scroll` · [scroll-storytelling](../preset/scroll-storytelling.md) |
|
|
35
|
+
| "futuristic / technical" | scramble, draw-on, mechanical easing, grid reveals | `text` + `svg` |
|
|
36
|
+
| "liquid / organic" | morph, path motion, overlapping soft easing | `svg` · [loader](../preset/loader.md) |
|
|
37
|
+
| "something crazy" | layered techniques with one clear concept | see below |
|
|
38
|
+
| "smooth image reveal" | clip-path or mask, not opacity | [reveal](../preset/reveal.md) |
|
|
39
|
+
| "the hero is boring" | hierarchy problem before a motion problem | `tune`, then `reveal` |
|
|
40
|
+
| "it feels janky" | cost or trigger problem | `audit` · [performance](performance.md) |
|
|
41
|
+
|
|
42
|
+
### "Something crazy"
|
|
43
|
+
|
|
44
|
+
A fade-and-scale is a failure of nerve; twelve techniques at once is a failure
|
|
45
|
+
of taste. Pick **one concept** and let three techniques serve it. The concept is
|
|
46
|
+
the thing you can say in a sentence — "the logo writes itself, then the dot that
|
|
47
|
+
drew it runs off and writes the word" — and every technique either serves that
|
|
48
|
+
sentence or is cut.
|
|
49
|
+
|
|
50
|
+
## Load map
|
|
51
|
+
|
|
52
|
+
The project's own rules come first, then the size tiers in SKILL.md, setup step
|
|
53
|
+
3: a one-element change loads [motion-design](motion-design.md) and the
|
|
54
|
+
command's own reference only. The table applies from component size up — a
|
|
55
|
+
single hover takes [interaction](interaction.md) and nothing else from its row.
|
|
56
|
+
|
|
57
|
+
| Request | Add |
|
|
58
|
+
|---|---|
|
|
59
|
+
| SVG draw / morph / path | [svg](svg.md), [timeline](timeline.md) |
|
|
60
|
+
| Scroll sequence, pin, scrub | [scrolltrigger](scrolltrigger.md), [timeline](timeline.md), [performance](performance.md) |
|
|
61
|
+
| Horizontal scroll | [scrolltrigger](scrolltrigger.md), [scroll-storytelling](../preset/scroll-storytelling.md) |
|
|
62
|
+
| Text reveal, kinetic type | [text](text.md), [timeline](timeline.md) |
|
|
63
|
+
| Magnetic, cursor, hover | [interaction](interaction.md), [performance](performance.md) |
|
|
64
|
+
| Drag, flick, inertia | [interaction](interaction.md) |
|
|
65
|
+
| Layout / shared element / modal | [flip](flip.md) |
|
|
66
|
+
| Page or route transition | [flip](flip.md), [page-transition](../preset/page-transition.md), this project's adapter |
|
|
67
|
+
| Loader, splash, intro | [svg](svg.md), [timeline](timeline.md), [accessibility](accessibility.md) |
|
|
68
|
+
| Marquee, orbit, ambient | [timeline](timeline.md), [performance](performance.md) |
|
|
69
|
+
| Three.js / R3F | [three](../adapter/three.md) or [r3f](../adapter/r3f.md), [performance](performance.md) |
|
|
70
|
+
| Audit an existing animation | [performance](performance.md), [accessibility](accessibility.md), this project's adapter |
|
|
71
|
+
| Retime / re-ease | [motion-design](motion-design.md) only |
|
|
72
|
+
|
|
73
|
+
Do not load a domain file "for completeness". Loading `adapter/three.md` for a
|
|
74
|
+
button hover is the failure this map exists to prevent.
|
|
75
|
+
|
|
76
|
+
## `audit`
|
|
77
|
+
|
|
78
|
+
**Run the checker first.** It is deterministic, it costs a second, and it finds
|
|
79
|
+
the mechanical failures before you spend attention on them:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node .claude/skills/gsap-creative-animation/scripts/audit-gsap.mjs <path>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**When an animation is not visible, ask whether it was ever built.** Before
|
|
86
|
+
touching timings or transforms, check that the code which creates it actually
|
|
87
|
+
runs. Animation is routinely built behind a condition — an image decode race, a
|
|
88
|
+
`ready.then()`, a ScrollTrigger that has not fired, a `matchMedia` branch that
|
|
89
|
+
does not match, an early `return` on a missing ref — and a tween that was never
|
|
90
|
+
created looks exactly like one that is mis-timed. A typical case: an image wipe
|
|
91
|
+
added only once the image decodes inside a short budget. On a first visit the
|
|
92
|
+
image is never cached, the branch never runs, and every hypothesis about the
|
|
93
|
+
wipe's geometry is wasted effort.
|
|
94
|
+
|
|
95
|
+
Then read for what it cannot see. Check in this order — the first two are where
|
|
96
|
+
real bugs live:
|
|
97
|
+
|
|
98
|
+
1. **Lifecycle** — is every animation inside a `useGSAP`/`gsap.context` scope?
|
|
99
|
+
Are ScrollTriggers killed? Are listeners removed? Does a `matchMedia` get
|
|
100
|
+
reverted? See this project's adapter.
|
|
101
|
+
2. **Cost** — layout-triggering properties, per-frame React state, filters on
|
|
102
|
+
large areas, one ScrollTrigger per item where one would do. See
|
|
103
|
+
[performance](performance.md).
|
|
104
|
+
3. **Reduced motion** — is there a branch, and does it still communicate?
|
|
105
|
+
4. **Craft** — rhythm, easing on loops, contrast. See [motion-design](motion-design.md).
|
|
106
|
+
|
|
107
|
+
Report findings most-severe first, with the file and line. Do not rewrite
|
|
108
|
+
unless asked.
|
|
109
|
+
|
|
110
|
+
## `strip`
|
|
111
|
+
|
|
112
|
+
Removing motion is a legitimate outcome and often the right one. Cut an
|
|
113
|
+
animation when it answers none of hierarchy, causality, continuity, state, or
|
|
114
|
+
personality; when it delays content the visitor came for; when it repeats on
|
|
115
|
+
every route and stopped being noticed on the second one; or when it exists
|
|
116
|
+
because a plugin was available.
|
|
117
|
+
|
|
118
|
+
Say what you removed and what it was costing. Do not replace it with a smaller
|
|
119
|
+
animation unless the user asks.
|
|
@@ -176,7 +176,7 @@ useGSAP(() => {
|
|
|
176
176
|
|
|
177
177
|
After a route change in the App Router, the new page's layout is measured before
|
|
178
178
|
images settle. `ScrollTrigger.refresh()` on mount is usually needed. See
|
|
179
|
-
[react
|
|
179
|
+
[react.md](../adapter/react.md).
|
|
180
180
|
|
|
181
181
|
## Failures
|
|
182
182
|
|
package/skill/reference/svg.md
CHANGED
|
@@ -24,7 +24,7 @@ as JSX.
|
|
|
24
24
|
|
|
25
25
|
Without them you end up at `querySelectorAll("path")[3]`, which breaks on the
|
|
26
26
|
next export. Prefix them, and namespace per instance — see
|
|
27
|
-
[react
|
|
27
|
+
[react.md](../adapter/react.md). For a set animated together, one
|
|
28
28
|
`data-*` attribute beats a run of ids:
|
|
29
29
|
|
|
30
30
|
```tsx
|
|
@@ -103,7 +103,7 @@ onMouseLeave = () => hover.reverse();
|
|
|
103
103
|
|
|
104
104
|
`reverse()` uses the same easing backwards, so the return matches the arrival
|
|
105
105
|
without a second definition. In React, anything built outside the `useGSAP`
|
|
106
|
-
body must go through `contextSafe` — see [react
|
|
106
|
+
body must go through `contextSafe` — see [react.md](../adapter/react.md).
|
|
107
107
|
|
|
108
108
|
## Holds
|
|
109
109
|
|