@mehshekari/gsap-motion 3.1.1 → 3.3.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 +1 -1
- package/bin/gsap-motion.mjs +17 -0
- package/package.json +1 -1
- package/skill/SKILL.md +50 -43
- 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/devices.md +110 -0
- package/skill/reference/interaction.md +183 -183
- package/skill/reference/motion-design.md +8 -5
- package/skill/reference/project-rules.md +16 -0
- package/skill/reference/refine.md +84 -0
- package/skill/reference/routing.md +136 -119
- package/skill/reference/scrolltrigger.md +1 -1
- package/skill/reference/svg.md +1 -1
- package/skill/reference/timeline.md +41 -1
- package/skill/scripts/lib/ast.mjs +7 -1
- package/skill/scripts/lib/rules.mjs +418 -13
- package/skill/scripts/lib/source.mjs +51 -3
- package/skill/reference/frameworks.md +0 -153
- package/skill/reference/react-nextjs.md +0 -234
- package/skill/reference/three-r3f.md +0 -161
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ The audit needs no install. Pin the version, so a new rule cannot fail your
|
|
|
28
28
|
build without warning:
|
|
29
29
|
|
|
30
30
|
```json
|
|
31
|
-
"lint": "eslint && npx @mehshekari/gsap-motion@3.
|
|
31
|
+
"lint": "eslint && npx @mehshekari/gsap-motion@3.3.0 audit src --quiet"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Or install it with `npm install --save-dev --save-exact @mehshekari/gsap-motion`.
|
package/bin/gsap-motion.mjs
CHANGED
|
@@ -257,6 +257,23 @@ function doctor() {
|
|
|
257
257
|
);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Which adapters the skill loads here, read the way SKILL.md's setup step 3
|
|
262
|
+
* describes. Deterministic, so the stack is looked up rather than guessed.
|
|
263
|
+
*/
|
|
264
|
+
const picked = new Set();
|
|
265
|
+
if (deps.next) picked.add("react").add("next");
|
|
266
|
+
if (deps["@react-three/fiber"]) picked.add("react").add("r3f");
|
|
267
|
+
else if (deps.three) picked.add("three");
|
|
268
|
+
if (deps.react) picked.add("react");
|
|
269
|
+
if (deps.vue || deps.nuxt) picked.add("vue");
|
|
270
|
+
if (deps.svelte || deps["@sveltejs/kit"]) picked.add("svelte");
|
|
271
|
+
if (deps.astro) picked.add("astro");
|
|
272
|
+
if (picked.size === 0) picked.add("vanilla");
|
|
273
|
+
|
|
274
|
+
const ORDER = ["react", "next", "r3f", "three", "vue", "svelte", "astro", "vanilla"];
|
|
275
|
+
report("ok", `Adapters: ${ORDER.filter((name) => picked.has(name)).join(", ")}`);
|
|
276
|
+
|
|
260
277
|
const rules = existsSync(join(process.cwd(), "ANIMATION.md"));
|
|
261
278
|
report(
|
|
262
279
|
rules ? "ok" : "info",
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Senior motion engineer for GSAP. Turns a visual or interaction idea
|
|
|
4
4
|
license: MIT. See LICENSE; GSAP itself is licensed separately, see NOTICE.md
|
|
5
5
|
compatibility: Any Agent Skills client; written for Claude Code. GSAP 3.13+, where every plugin is free. React guidance targets @gsap/react 2.x, React 18/19 and the Next.js App Router. The audit scripts need Node.js 18+ and nothing else.
|
|
6
6
|
metadata:
|
|
7
|
-
version: "3.
|
|
7
|
+
version: "3.3.0"
|
|
8
8
|
verified-gsap: "3.15"
|
|
9
9
|
verified-gsap-react: "2.1"
|
|
10
10
|
---
|
|
@@ -18,8 +18,8 @@ Core principles:
|
|
|
18
18
|
|
|
19
19
|
- **Motion carries meaning or it goes.** Every animation answers "what does
|
|
20
20
|
this tell the visitor?" Hierarchy, causality, continuity, state, or
|
|
21
|
-
personality.
|
|
22
|
-
|
|
21
|
+
personality. One that answers none of those is decoration with a frame cost:
|
|
22
|
+
delete it.
|
|
23
23
|
- **A timeline, not a pile of tweens.** Five independent tweens with hand-tuned
|
|
24
24
|
delays is a choreography nobody can change. One timeline with labels and
|
|
25
25
|
relative positions is the same animation you can actually retime.
|
|
@@ -33,23 +33,27 @@ Core principles:
|
|
|
33
33
|
|
|
34
34
|
## Setup
|
|
35
35
|
|
|
36
|
-
1. **Everything is free.** GSAP became 100% free
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
1. **Everything is free.** GSAP became 100% free with 3.13 (April 2025), every
|
|
37
|
+
former Club plugin included. Check `package.json` for what the project has —
|
|
38
|
+
`gsap`, and `@gsap/react` for React — before writing imports. Install steps
|
|
39
|
+
for a membership, a token, an `.npmrc` or `gsap-bonus.tgz` are stale
|
|
40
|
+
training data: stop.
|
|
41
41
|
2. **Read the project's own animation rules first. They outrank this skill.**
|
|
42
|
-
Look for `ANIMATION.md` at the
|
|
43
|
-
|
|
42
|
+
Look for `ANIMATION.md` at the root, then a file `AGENTS.md`, `CLAUDE.md` or
|
|
43
|
+
similar points to. Open
|
|
44
44
|
[reference/project-rules.md](reference/project-rules.md) only when there is
|
|
45
|
-
|
|
45
|
+
none, or for its RTL, colour and waiver rules.
|
|
46
46
|
3. **Load by the size of the request, and nothing "for completeness".**
|
|
47
47
|
- **One element, one tween or interaction** — a hover, a fade, a retime:
|
|
48
48
|
[motion-design.md](reference/motion-design.md) and the command's own
|
|
49
49
|
reference. Nothing else.
|
|
50
|
-
- **A component or section:** also
|
|
51
|
-
[react
|
|
52
|
-
|
|
50
|
+
- **A component or section:** also this project's adapter, from
|
|
51
|
+
`package.json`: `next` → [react](adapter/react.md) +
|
|
52
|
+
[next](adapter/next.md); `@react-three/fiber` → react +
|
|
53
|
+
[r3f](adapter/r3f.md); `three` → [three](adapter/three.md); `nuxt` →
|
|
54
|
+
[vue](adapter/vue.md); `svelte` → [svelte](adapter/svelte.md); `astro` →
|
|
55
|
+
[astro](adapter/astro.md) plus any island's own; nothing detected →
|
|
56
|
+
[vanilla](adapter/vanilla.md).
|
|
53
57
|
- **A sequence, scroll scene, intro, transition or audit:** also
|
|
54
58
|
[core-gsap.md](reference/core-gsap.md), and what
|
|
55
59
|
[routing.md](reference/routing.md) adds for that request.
|
|
@@ -68,7 +72,7 @@ Core principles:
|
|
|
68
72
|
| `transition [target]` | Build | Page, route, modal and layout transitions | [reference/flip.md](reference/flip.md) |
|
|
69
73
|
| `loader` | Build | Loading and progress animation | [preset/loader.md](preset/loader.md) |
|
|
70
74
|
| `intro` | Build | Site or section entry sequence | [preset/cinematic.md](preset/cinematic.md) |
|
|
71
|
-
| `three [target]` | Build | GSAP orchestrating Three.js / R3F | [
|
|
75
|
+
| `three [target]` | Build | GSAP orchestrating Three.js / R3F | [adapter/three.md](adapter/three.md) |
|
|
72
76
|
| `audit [target]` | Evaluate | Check an existing animation: leaks, cost, a11y | [reference/performance.md](reference/performance.md) |
|
|
73
77
|
| `tune [target]` | Refine | Retime, re-ease, fix rhythm — no new motion | [reference/motion-design.md](reference/motion-design.md) |
|
|
74
78
|
| `strip [target]` | Fix | Remove motion that is not earning its place | [reference/motion-design.md](reference/motion-design.md) |
|
|
@@ -76,12 +80,11 @@ Core principles:
|
|
|
76
80
|
Routing:
|
|
77
81
|
|
|
78
82
|
- **No argument, or no target:** read [routing.md](reference/routing.md) and
|
|
79
|
-
ask what should move.
|
|
80
|
-
|
|
81
|
-
- **
|
|
82
|
-
- **A description
|
|
83
|
-
|
|
84
|
-
table, then load what that classification needs.
|
|
83
|
+
ask what should move. "Add some animation to my homepage" names no target:
|
|
84
|
+
never invent one.
|
|
85
|
+
- **A command, explicit or implied:** load its reference and follow it.
|
|
86
|
+
- **A description** ("make this feel like Apple", "the hero is boring"): that is
|
|
87
|
+
`animate`. Classify with routing.md's table, then load what that needs.
|
|
85
88
|
- **Two commands fit:** ask once. A scroll-driven text reveal is `scroll` and
|
|
86
89
|
`text` together, not a coin toss.
|
|
87
90
|
|
|
@@ -109,7 +112,8 @@ intent → precedent → targets → trigger → motion language → technique
|
|
|
109
112
|
8. **Implementation** — typed, cleaned up, responsive.
|
|
110
113
|
9. **Performance** — transform and opacity; measure before defending anything else.
|
|
111
114
|
10. **Accessibility** — the reduced-motion design, focus, keyboard.
|
|
112
|
-
11. **Validate** —
|
|
115
|
+
11. **Validate** — [refine.md](reference/refine.md): audit, fix in order,
|
|
116
|
+
re-run, then the checklist in [motion-design.md](reference/motion-design.md).
|
|
113
117
|
|
|
114
118
|
## Output format
|
|
115
119
|
|
|
@@ -122,26 +126,29 @@ For anything beyond a one-line tweak, answer in this shape, each part short:
|
|
|
122
126
|
- **Implementation** — complete code. Not pseudocode, not an outline.
|
|
123
127
|
- **Usage** — how to mount it.
|
|
124
128
|
- **Notes** — responsive behaviour, reduced motion, cost, dev-only tooling.
|
|
129
|
+
- **Verification** — one line per check with its evidence, and what you did not
|
|
130
|
+
check. Silence reads as "checked and fine".
|
|
125
131
|
|
|
126
|
-
When a simpler technique does what
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
When a simpler technique does what was asked, **write only that one**. Say in
|
|
133
|
+
Analysis what you declined and why, and offer the requested version in a
|
|
134
|
+
sentence — write it only if they still want it. Never substitute quietly, and
|
|
135
|
+
never ship both.
|
|
130
136
|
|
|
131
137
|
## Reference map
|
|
132
138
|
|
|
133
139
|
- **Craft and core** — [motion-design](reference/motion-design.md) ·
|
|
134
|
-
[core-gsap](reference/core-gsap.md) · [timeline](reference/timeline.md)
|
|
140
|
+
[core-gsap](reference/core-gsap.md) · [timeline](reference/timeline.md) ·
|
|
141
|
+
[devices](reference/devices.md) · [refine](reference/refine.md)
|
|
135
142
|
- **Domains** — [scrolltrigger](reference/scrolltrigger.md) ·
|
|
136
143
|
[svg](reference/svg.md) · [text](reference/text.md) ·
|
|
137
|
-
[interaction](reference/interaction.md) · [flip](reference/flip.md)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
[interaction](reference/interaction.md) · [flip](reference/flip.md)
|
|
145
|
+
- **Adapters**, one per stack, chosen in setup step 3 — the same questions for
|
|
146
|
+
each: lifecycle, scope, server rendering, per-frame values, routes, failures,
|
|
147
|
+
audit.
|
|
148
|
+
- **Constraints** — [performance](reference/performance.md) ·
|
|
142
149
|
[accessibility](reference/accessibility.md) ·
|
|
143
150
|
[project-rules](reference/project-rules.md)
|
|
144
|
-
- **Presets
|
|
151
|
+
- **Presets** to adapt — [reveal](preset/reveal.md) ·
|
|
145
152
|
[magnetic](preset/magnetic.md) · [cursor](preset/cursor.md) ·
|
|
146
153
|
[marquee](preset/marquee.md) · [card-stack](preset/card-stack.md) ·
|
|
147
154
|
[loader](preset/loader.md) · [cinematic](preset/cinematic.md) ·
|
|
@@ -156,13 +163,12 @@ after that. Never quietly substitute, and never ship both.
|
|
|
156
163
|
|
|
157
164
|
## Tools
|
|
158
165
|
|
|
159
|
-
Both scripts catch
|
|
160
|
-
|
|
166
|
+
Both scripts catch **silent** failures. Run them from the project root;
|
|
167
|
+
`<skill-dir>` is where this skill is installed.
|
|
161
168
|
|
|
162
169
|
```bash
|
|
163
|
-
# Leaks, per-event tweens, layout properties,
|
|
164
|
-
#
|
|
165
|
-
# Exit 1 on an error.
|
|
170
|
+
# Leaks, per-event and per-frame tweens, layout and paint properties, loops,
|
|
171
|
+
# shared plugin ids, unregistered plugins, dev tooling. Exit 1 on an error.
|
|
166
172
|
node <skill-dir>/scripts/audit-gsap.mjs [path...] # default: src
|
|
167
173
|
node <skill-dir>/scripts/audit-gsap.mjs src --quiet # errors only
|
|
168
174
|
node <skill-dir>/scripts/audit-gsap.mjs src --json # machine-readable
|
|
@@ -173,10 +179,11 @@ node <skill-dir>/scripts/audit-svg.mjs --morph <a.svg> <b.svg>
|
|
|
173
179
|
node <skill-dir>/scripts/audit-svg.mjs --hues <file.svg> # also flag hue literals
|
|
174
180
|
```
|
|
175
181
|
|
|
176
|
-
Run `audit-gsap` first in an `audit` and after writing any animation
|
|
177
|
-
|
|
182
|
+
Run `audit-gsap` first in an `audit` and after writing any animation — as a
|
|
183
|
+
Claude Code plugin, a hook already runs it on every edit and hands back what it
|
|
184
|
+
finds. A false finding is a bug in the script, not something to work around.
|
|
178
185
|
|
|
179
186
|
`MotionPathHelper`, `GSDevTools`, `markers: true` and
|
|
180
|
-
`MorphSVGPlugin.findShapeIndex()` are development-only
|
|
181
|
-
whatever `if` surrounds it
|
|
182
|
-
and delete the block once their value is baked in.
|
|
187
|
+
`MorphSVGPlugin.findShapeIndex()` are development-only, and a static import
|
|
188
|
+
ships whatever `if` surrounds it. Import them dynamically behind a `NODE_ENV`
|
|
189
|
+
check, and delete the block once their value is baked in.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Astro
|
|
2
|
+
|
|
3
|
+
Astro ships no JavaScript unless you ask for it, and its pages can be swapped
|
|
4
|
+
without a reload. Both facts decide where an animation lives.
|
|
5
|
+
|
|
6
|
+
## Where it is created
|
|
7
|
+
|
|
8
|
+
In a component's `<script>`, which Astro bundles and defers. With view
|
|
9
|
+
transitions enabled, a page swap does not re-run it, so build on the page-load
|
|
10
|
+
event rather than at the top level:
|
|
11
|
+
|
|
12
|
+
```astro
|
|
13
|
+
<script>
|
|
14
|
+
import gsap from "gsap";
|
|
15
|
+
|
|
16
|
+
let ctx;
|
|
17
|
+
|
|
18
|
+
document.addEventListener("astro:page-load", () => {
|
|
19
|
+
ctx = gsap.context(() => {
|
|
20
|
+
gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
document.addEventListener("astro:before-swap", () => ctx?.revert());
|
|
25
|
+
</script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`astro:page-load` fires on the first load and after every swap, so this one
|
|
29
|
+
shape covers both. Without view transitions the event still fires once, so the
|
|
30
|
+
same code is correct either way.
|
|
31
|
+
|
|
32
|
+
Inside a React, Vue or Svelte island, use that framework's adapter instead —
|
|
33
|
+
the island owns its own lifecycle.
|
|
34
|
+
|
|
35
|
+
## Where it is torn down
|
|
36
|
+
|
|
37
|
+
`astro:before-swap`, with `ctx.revert()`. Skipping it leaves the previous page's
|
|
38
|
+
ScrollTriggers and listeners bound to elements that are about to be replaced,
|
|
39
|
+
and they accumulate for as long as the visit lasts.
|
|
40
|
+
|
|
41
|
+
Timers, listeners and observers are yours, in the same handler.
|
|
42
|
+
|
|
43
|
+
## Scope
|
|
44
|
+
|
|
45
|
+
Astro's `<script>` is page-wide, not component-scoped: a component used twice on
|
|
46
|
+
a page runs its script once, and selector strings match both instances. Either
|
|
47
|
+
animate them as a set — which is usually what you want — or pass the root in
|
|
48
|
+
with a `data-*` attribute and build one context per matching element.
|
|
49
|
+
|
|
50
|
+
Plugin config is not scoped either, so generate ids per instance rather than
|
|
51
|
+
hardcoding them.
|
|
52
|
+
|
|
53
|
+
## Server rendering and hydration
|
|
54
|
+
|
|
55
|
+
Astro components render to HTML on the server and ship no client JavaScript of
|
|
56
|
+
their own, so there is no hydration to mismatch. The script runs only in the
|
|
57
|
+
browser: reading `window` in it is safe, and the markup it animates is already
|
|
58
|
+
painted.
|
|
59
|
+
|
|
60
|
+
`client:*` directives decide when an island hydrates. An animation inside one
|
|
61
|
+
does not run until it does — `client:visible` on a section that animates on
|
|
62
|
+
entrance is a race worth avoiding; prefer `client:load` there, or keep the
|
|
63
|
+
animation in the page's own script.
|
|
64
|
+
|
|
65
|
+
## Reaching the element
|
|
66
|
+
|
|
67
|
+
`data-*` attributes in the markup, queried inside the context. Astro scopes its
|
|
68
|
+
component styles by adding its own attributes to elements, so a class in the
|
|
69
|
+
markup is not always the class in the DOM — a `data-*` attribute you wrote is.
|
|
70
|
+
|
|
71
|
+
## Every frame
|
|
72
|
+
|
|
73
|
+
`gsap.quickTo` or `quickSetter`, created once inside the context. There is no
|
|
74
|
+
component state here to get wrong, which removes the most common React mistake
|
|
75
|
+
and leaves the ordinary one: allocating a tween per event.
|
|
76
|
+
|
|
77
|
+
## Page and route changes
|
|
78
|
+
|
|
79
|
+
The events are the contract:
|
|
80
|
+
|
|
81
|
+
- `astro:page-load` — build, on first load and after every swap
|
|
82
|
+
- `astro:before-swap` — revert, before the old page goes
|
|
83
|
+
- `astro:after-swap` — for anything that must run against the new DOM before it
|
|
84
|
+
paints, such as `ScrollTrigger.refresh()`
|
|
85
|
+
|
|
86
|
+
View transitions are opt-in: they come from Astro's router component in the
|
|
87
|
+
layout, named `<ClientRouter />` in current versions and `<ViewTransitions />`
|
|
88
|
+
in older ones. Check which one the project imports before telling anyone to add
|
|
89
|
+
it.
|
|
90
|
+
|
|
91
|
+
## Failures
|
|
92
|
+
|
|
93
|
+
| Symptom | Cause |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Animation runs on the first page only | built at the top of `<script>` instead of on `astro:page-load` |
|
|
96
|
+
| Triggers pile up while browsing | no `astro:before-swap` revert |
|
|
97
|
+
| The script never runs | `is:inline`, which skips bundling, so the bare import fails |
|
|
98
|
+
| Both instances animate as one | a page-wide script with a document-wide selector |
|
|
99
|
+
| An entrance animation starts late | the island hydrates on `client:visible` |
|
|
100
|
+
|
|
101
|
+
## What the audit covers
|
|
102
|
+
|
|
103
|
+
From 3.2 the audit reads the `<script>` block of `.astro` files, with offsets
|
|
104
|
+
kept, so reported lines match the file. It checks that a context built on
|
|
105
|
+
`astro:page-load` is reverted on `astro:before-swap`, along with every rule that
|
|
106
|
+
is not React-specific.
|
|
107
|
+
|
|
108
|
+
## Versions
|
|
109
|
+
|
|
110
|
+
Verified against `gsap@3.15`. Written for `astro@7`; the swap events are the
|
|
111
|
+
same in Astro 5 and later, though the router component was renamed.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Next.js
|
|
2
|
+
|
|
3
|
+
The App Router, with Server Components. Read [react.md](react.md) first: this is
|
|
4
|
+
only what Next adds.
|
|
5
|
+
|
|
6
|
+
## Where it is created
|
|
7
|
+
|
|
8
|
+
In a `"use client"` component, in the `useGSAP` body react.md describes. Keep
|
|
9
|
+
that component a **leaf**, so the page above it stays a Server Component.
|
|
10
|
+
|
|
11
|
+
**Converting a Server Component is part of the cost, and it is priced before the
|
|
12
|
+
design.** *Is GSAP already on this route?* If a sibling section loads `gsap` and
|
|
13
|
+
`ScrollTrigger`, the marginal cost is this component's own JavaScript — a
|
|
14
|
+
kilobyte or two rather than thirty-odd, which usually settles the argument. If
|
|
15
|
+
nothing on the route loads GSAP, this animation buys the whole library for one
|
|
16
|
+
reveal. *Does anything claim otherwise?* A comment saying the section ships no
|
|
17
|
+
client JavaScript stops being true the moment you add the hook; update it in the
|
|
18
|
+
same change.
|
|
19
|
+
|
|
20
|
+
Say so in the Notes of your answer: "it becomes a client component" is a fact
|
|
21
|
+
the reader needs.
|
|
22
|
+
|
|
23
|
+
## Where it is torn down
|
|
24
|
+
|
|
25
|
+
By the context, as in react.md. A navigation unmounts the page, so anything that
|
|
26
|
+
must survive one belongs in the layout.
|
|
27
|
+
|
|
28
|
+
## Scope
|
|
29
|
+
|
|
30
|
+
As in react.md, and `useId` matters more here: the markup is rendered on the
|
|
31
|
+
server and again on the client, and a counter would disagree between them.
|
|
32
|
+
|
|
33
|
+
## Server rendering and hydration
|
|
34
|
+
|
|
35
|
+
- **`"use client"` module scope still runs during server rendering.**
|
|
36
|
+
Registering plugins there is fine — GSAP defers anything touching `window` —
|
|
37
|
+
but reading `window` or `document` at module scope is not.
|
|
38
|
+
- **Data and copy come down as props.** A client component importing a content
|
|
39
|
+
or locale module bundles every locale into the browser payload. Large
|
|
40
|
+
generated data — path tables, keyframe sets — is handed down too.
|
|
41
|
+
- Storage and media queries are read after mount, never during render.
|
|
42
|
+
|
|
43
|
+
## Reaching the element
|
|
44
|
+
|
|
45
|
+
`next/image` renders the `<img>` itself and controls its `src`, `srcset` and
|
|
46
|
+
sizing:
|
|
47
|
+
|
|
48
|
+
- **Animate a wrapper you own**, not the image, when the effect needs a mask, a
|
|
49
|
+
clip or a transform that must survive the placeholder-to-loaded swap. Under
|
|
50
|
+
`fill` the image is positioned against the nearest positioned ancestor, so
|
|
51
|
+
that wrapper is already there.
|
|
52
|
+
- **A reveal that depends on the image waits for load**, with a short budget
|
|
53
|
+
after which it plays anyway. A cached image can finish before the hook runs,
|
|
54
|
+
and a mask opening over an empty box reads as a bug.
|
|
55
|
+
- If the animation starts by hiding a hero image, **measure what it does to
|
|
56
|
+
LCP** first. A mask over an image that is already painted usually avoids the
|
|
57
|
+
question.
|
|
58
|
+
|
|
59
|
+
## Every frame
|
|
60
|
+
|
|
61
|
+
Refs and `quickTo`, never React state — and it costs more here, since state held
|
|
62
|
+
in a layout re-renders every page under it.
|
|
63
|
+
|
|
64
|
+
## Page and route changes
|
|
65
|
+
|
|
66
|
+
The App Router unmounts the old tree before the new one paints, so an exit
|
|
67
|
+
animation has nowhere to play. Options, by cost:
|
|
68
|
+
|
|
69
|
+
1. **Animate entrance only**, keyed off `usePathname()`. Honest, cheap, and what
|
|
70
|
+
most sites want.
|
|
71
|
+
2. **An overlay that outlives the route**, mounted in the layout so the
|
|
72
|
+
navigation does not unmount it. See
|
|
73
|
+
[page-transition](../preset/page-transition.md).
|
|
74
|
+
3. **`router.push` behind a played-out timeline** — intercept, play the exit,
|
|
75
|
+
navigate on complete. Breaks the back button's feel; use sparingly.
|
|
76
|
+
|
|
77
|
+
Then `ScrollTrigger.refresh()` once the new layout settles.
|
|
78
|
+
|
|
79
|
+
## Failures
|
|
80
|
+
|
|
81
|
+
| Symptom | Cause |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `window is not defined` during a build | `window` read at module scope in a client component |
|
|
84
|
+
| Hydration mismatch | storage, a media query or a random value read during render |
|
|
85
|
+
| The whole page ships as client JavaScript | `"use client"` on the page or layout instead of a leaf |
|
|
86
|
+
| A reveal opens over an empty box | the tween did not wait for the image to load |
|
|
87
|
+
| Positions are wrong after a navigation | no `ScrollTrigger.refresh()` once it settled |
|
|
88
|
+
| The exit animation never plays | written in the page the navigation unmounts |
|
|
89
|
+
|
|
90
|
+
## What the audit covers
|
|
91
|
+
|
|
92
|
+
The React rules from react.md, on the same files. It reads `.ts`, `.tsx`, `.js`,
|
|
93
|
+
`.jsx` and `.mjs`, so `"use client"` placement, `next.config` and route
|
|
94
|
+
structure are yours to review.
|
|
95
|
+
|
|
96
|
+
## Versions
|
|
97
|
+
|
|
98
|
+
Verified against `gsap@3.15` and `@gsap/react@2.1`. Written for `next@15` and
|
|
99
|
+
`next@16`, App Router.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# React Three Fiber
|
|
2
|
+
|
|
3
|
+
R3F is React's renderer for Three, so both lifecycles apply: read
|
|
4
|
+
[react.md](react.md) for the context and cleanup rules, and
|
|
5
|
+
[three.md](three.md) for what GSAP may tween on a Three object. This file is the
|
|
6
|
+
part that is neither.
|
|
7
|
+
|
|
8
|
+
## Where it is created
|
|
9
|
+
|
|
10
|
+
In a `useGSAP` body inside the `<Canvas>` subtree, against refs to the objects:
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
"use client";
|
|
14
|
+
|
|
15
|
+
import { useGSAP } from "@gsap/react";
|
|
16
|
+
import gsap from "gsap";
|
|
17
|
+
import { useRef } from "react";
|
|
18
|
+
import type { Mesh } from "three";
|
|
19
|
+
|
|
20
|
+
export function Box() {
|
|
21
|
+
const mesh = useRef<Mesh>(null);
|
|
22
|
+
|
|
23
|
+
useGSAP(() => {
|
|
24
|
+
if (!mesh.current) return;
|
|
25
|
+
gsap.to(mesh.current.rotation, {
|
|
26
|
+
y: Math.PI * 2,
|
|
27
|
+
duration: 6,
|
|
28
|
+
ease: "none",
|
|
29
|
+
repeat: -1,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<mesh ref={mesh}>
|
|
35
|
+
<boxGeometry />
|
|
36
|
+
<meshStandardMaterial />
|
|
37
|
+
</mesh>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A camera move is where GSAP earns its place — easing a camera by hand is how
|
|
43
|
+
scroll-driven 3D ends up feeling mechanical:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const { camera, invalidate } = useThree();
|
|
47
|
+
|
|
48
|
+
gsap.timeline({ defaults: { ease: "power2.inOut", duration: 2 } })
|
|
49
|
+
.to(camera.position, { x: 0, y: 1.5, z: 4 })
|
|
50
|
+
.to(camera.rotation, { x: -0.2 }, "<");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Where it is torn down
|
|
54
|
+
|
|
55
|
+
The `useGSAP` context reverts the tweens, as in react.md.
|
|
56
|
+
|
|
57
|
+
R3F disposes what it created for elements it rendered, when they unmount.
|
|
58
|
+
Anything you built yourself — a geometry, material or texture made outside JSX,
|
|
59
|
+
a loader's result you cached — is yours to dispose.
|
|
60
|
+
|
|
61
|
+
## Scope
|
|
62
|
+
|
|
63
|
+
There are no selector strings here: targets are refs. `{ scope: root }` still
|
|
64
|
+
matters for any DOM overlay the component renders beside the canvas.
|
|
65
|
+
|
|
66
|
+
## Server rendering and hydration
|
|
67
|
+
|
|
68
|
+
The canvas is client-only. Import the component dynamically so Three is not in
|
|
69
|
+
the initial bundle, and render a fallback while it loads — that fallback is also
|
|
70
|
+
the reduced-motion and low-power design, so it is not wasted work.
|
|
71
|
+
|
|
72
|
+
## Reaching the element
|
|
73
|
+
|
|
74
|
+
Refs, held where the object is declared. `useThree` reaches the camera, the
|
|
75
|
+
renderer and the scene without prop-drilling. Traversing by name binds to the
|
|
76
|
+
exporter's names, which the next export changes.
|
|
77
|
+
|
|
78
|
+
## Every frame
|
|
79
|
+
|
|
80
|
+
**Never drive a 3D transform through React state.** `useState` per frame
|
|
81
|
+
re-renders the React tree for a value Three reads directly. Refs and GSAP only.
|
|
82
|
+
|
|
83
|
+
`useFrame` and GSAP are complementary, not competing: `useFrame` for continuous
|
|
84
|
+
logic that follows the clock, GSAP for transitions between states. Inside
|
|
85
|
+
`useFrame`, the rules are the ones for any per-frame scope — no allocation, no
|
|
86
|
+
state, no `setState` from a callback.
|
|
87
|
+
|
|
88
|
+
With `frameloop="demand"`, the canvas renders only when something asks it to, so
|
|
89
|
+
a GSAP tween that changes a Three value must ask:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
gsap.to(mesh.current.position, { y: 1, onUpdate: invalidate });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Without that, the tween runs and nothing is drawn — the most confusing failure
|
|
96
|
+
in this file, because every value is correct.
|
|
97
|
+
|
|
98
|
+
## Page and route changes
|
|
99
|
+
|
|
100
|
+
The canvas unmounts with the route, and R3F disposes its own objects. What
|
|
101
|
+
survives is anything you created outside JSX, plus any ScrollTrigger built
|
|
102
|
+
against a DOM element that is now gone — which the context reverts, provided it
|
|
103
|
+
was created inside the body.
|
|
104
|
+
|
|
105
|
+
## Failures
|
|
106
|
+
|
|
107
|
+
| Symptom | Cause |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Values change but nothing is drawn | `frameloop="demand"` with no `invalidate` from `onUpdate` |
|
|
110
|
+
| The whole tree re-renders while animating | 3D values held in React state |
|
|
111
|
+
| Nothing moves | tweened the object instead of `object.position` / `.rotation` |
|
|
112
|
+
| The tween starts before the object exists | no early return on a null ref |
|
|
113
|
+
| Memory grows across route changes | objects created outside JSX, never disposed |
|
|
114
|
+
| Scroll-driven camera drifts out of sync | a scrub with an ease on a child, instead of `ease: "none"` |
|
|
115
|
+
|
|
116
|
+
## What the audit covers
|
|
117
|
+
|
|
118
|
+
The React rules from react.md, since these are `.tsx` files. From 3.2,
|
|
119
|
+
`useFrame` counts as a per-frame scope, so React state set inside it is reported
|
|
120
|
+
as `state-per-event`.
|
|
121
|
+
|
|
122
|
+
Disposal, GPU cost and the render loop are not checkable — read for them, and
|
|
123
|
+
measure with [performance.md](../reference/performance.md).
|
|
124
|
+
|
|
125
|
+
## Versions
|
|
126
|
+
|
|
127
|
+
Verified against `gsap@3.15` and `@gsap/react@2.1`. Written for
|
|
128
|
+
`@react-three/fiber@8` and `@react-three/fiber@9`.
|