@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# React
|
|
2
|
+
|
|
3
|
+
Animation is a client concern, and the interesting problems are all lifecycle.
|
|
4
|
+
This covers any React app — Vite, Remix, Next. With Next, read
|
|
5
|
+
[next.md](next.md) too; with React Three Fiber, [r3f.md](r3f.md).
|
|
6
|
+
|
|
7
|
+
## Where it is created
|
|
8
|
+
|
|
9
|
+
`useGSAP`, from `@gsap/react`: `useLayoutEffect` with an SSR fallback, plus
|
|
10
|
+
automatic `gsap.context()` cleanup — which is what makes it survive StrictMode's
|
|
11
|
+
double invoke in development.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { useGSAP } from "@gsap/react";
|
|
15
|
+
import gsap from "gsap";
|
|
16
|
+
import { useRef } from "react";
|
|
17
|
+
|
|
18
|
+
gsap.registerPlugin(useGSAP); // module scope, once
|
|
19
|
+
|
|
20
|
+
export function useReveal() {
|
|
21
|
+
const root = useRef<HTMLDivElement>(null);
|
|
22
|
+
|
|
23
|
+
useGSAP(() => {
|
|
24
|
+
gsap.from("[data-item]", { y: 20, stagger: 0.05 }); // scoped to root
|
|
25
|
+
}, { scope: root });
|
|
26
|
+
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
It is a layout effect, so it runs **before paint**: a `from` or a `set` lands
|
|
32
|
+
with no flash of the end state. Its options are `scope`, `dependencies` (the
|
|
33
|
+
body re-runs on change) and `revertOnUpdate` (the previous run reverts first).
|
|
34
|
+
|
|
35
|
+
A tween created *outside* the body — in a handler, a promise, a callback — is
|
|
36
|
+
never reverted. Wrap that function in `contextSafe`, from the same hook, and the
|
|
37
|
+
context owns it again. Only wrap functions that *create* animations: playing an
|
|
38
|
+
existing timeline creates nothing. `contextSafe` runs during render, so reading a
|
|
39
|
+
ref inside the callback trips React's "cannot access refs during render" rule —
|
|
40
|
+
build the timeline in the body, keep it on a ref, and expose a `useCallback`
|
|
41
|
+
that plays it.
|
|
42
|
+
|
|
43
|
+
## Where it is torn down
|
|
44
|
+
|
|
45
|
+
`useGSAP` reverts its context on unmount. Tweens, timelines, `ScrollTrigger`,
|
|
46
|
+
`Observer`, `Draggable`, `SplitText` and `gsap.matchMedia()` register themselves
|
|
47
|
+
with the context while its function runs and are reverted with it, so none needs
|
|
48
|
+
a manual `kill()` inside the body.
|
|
49
|
+
|
|
50
|
+
**These are yours**, because GSAP never sees them: timers, listeners on `window`
|
|
51
|
+
or `document`, `ResizeObserver` and `IntersectionObserver` — and **anything
|
|
52
|
+
created outside the body**, at module scope, in a plain `useEffect`, or in a
|
|
53
|
+
handler that never went through `contextSafe`. That last one is the real leak,
|
|
54
|
+
and the one people write cleanup for the wrong things instead of catching.
|
|
55
|
+
|
|
56
|
+
## Scope
|
|
57
|
+
|
|
58
|
+
`{ scope: root }` is not optional in practice: selector strings resolve inside
|
|
59
|
+
the component, so two instances cannot animate each other's elements.
|
|
60
|
+
|
|
61
|
+
A scope does **not** reach inside plugin config: `motionPath.path` and
|
|
62
|
+
`morphSVG.shape` resolve against the whole document, so a hardcoded id in a
|
|
63
|
+
component rendered twice makes both instances drive the first one's geometry,
|
|
64
|
+
silently. Generate one per instance from `useId()`, stripped to word characters
|
|
65
|
+
— React's id carries delimiters a CSS selector cannot hold, and unlike a counter
|
|
66
|
+
it is stable across server and client.
|
|
67
|
+
|
|
68
|
+
## Server rendering and hydration
|
|
69
|
+
|
|
70
|
+
Reading `localStorage`, `sessionStorage` or `matchMedia` **during render** makes
|
|
71
|
+
the server and the client disagree. Render the neutral state and correct it in
|
|
72
|
+
the layout effect, before paint, so nothing is seen.
|
|
73
|
+
|
|
74
|
+
Read direction and preferences from the DOM inside the effect, never from a prop:
|
|
75
|
+
`document.documentElement.dir === "rtl"` keeps the hook independent of the i18n
|
|
76
|
+
library, and the layout stamps `dir` before first paint.
|
|
77
|
+
|
|
78
|
+
## Reaching the element
|
|
79
|
+
|
|
80
|
+
A shared component that does not forward props has nowhere to hang a `data-*`
|
|
81
|
+
attribute; widening it to take its element's remaining props is usually right,
|
|
82
|
+
and often already the house pattern. A positional selector
|
|
83
|
+
(`row.firstElementChild`) breaks the next time the markup moves.
|
|
84
|
+
|
|
85
|
+
## Every frame
|
|
86
|
+
|
|
87
|
+
React state for a value that changes every frame is a re-render per frame.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// Wrong
|
|
91
|
+
const [x, setX] = useState(0);
|
|
92
|
+
onMouseMove = (e) => setX(e.clientX);
|
|
93
|
+
|
|
94
|
+
// Right
|
|
95
|
+
const moveX = gsap.quickTo(el.current, "x", { duration: 0.5, ease: "power3" });
|
|
96
|
+
onMouseMove = (e) => moveX(e.clientX);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
State is for *what exists* — is the modal open, which tab is active. GSAP owns
|
|
100
|
+
*how it moves*. Crossing that line is the most common performance bug in
|
|
101
|
+
animated React.
|
|
102
|
+
|
|
103
|
+
## Page and route changes
|
|
104
|
+
|
|
105
|
+
A client router unmounts the outgoing tree, so an exit animation needs something
|
|
106
|
+
that outlives the route — an overlay in the layout, not in the page. See
|
|
107
|
+
[page-transition](../preset/page-transition.md), and [next.md](next.md) for the
|
|
108
|
+
App Router. After a navigation, `ScrollTrigger.refresh()` once it settles.
|
|
109
|
+
|
|
110
|
+
## Failures
|
|
111
|
+
|
|
112
|
+
| Symptom | Cause |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Animation runs twice in dev | tweens created outside `useGSAP` — StrictMode |
|
|
115
|
+
| Animation leaks after navigation | ScrollTrigger or listener created outside the scope |
|
|
116
|
+
| Flash of the end state | `from` outside a layout effect |
|
|
117
|
+
| Two components animate each other | missing `scope`, or a shared hardcoded id |
|
|
118
|
+
| Hydration mismatch | storage or a media query read during render |
|
|
119
|
+
| Janky pointer effect | React state in the handler |
|
|
120
|
+
| Hover sticks halfway | no `overwrite: "auto"` on the retriggered tween |
|
|
121
|
+
|
|
122
|
+
## What the audit covers
|
|
123
|
+
|
|
124
|
+
`orphan-tween`, `state-per-event` and `shared-plugin-id` are React-specific: the
|
|
125
|
+
audit applies them to `.jsx` and `.tsx` files, and to any file importing `react`
|
|
126
|
+
or `@gsap/react`. Every other rule applies to every file it reads. It follows a
|
|
127
|
+
helper within one file — a function only a `useGSAP` body calls counts as inside
|
|
128
|
+
the context — but never into another file.
|
|
129
|
+
|
|
130
|
+
## Versions
|
|
131
|
+
|
|
132
|
+
Verified against `gsap@3.15` and `@gsap/react@2.1`. Written for `react@18` and
|
|
133
|
+
`react@19`.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Svelte and SvelteKit
|
|
2
|
+
|
|
3
|
+
Svelte 5 runs effects in runes mode; `onMount` still works and reads the same in
|
|
4
|
+
both versions. Either way the animation is created after mount, in a context
|
|
5
|
+
that is reverted when the component is destroyed.
|
|
6
|
+
|
|
7
|
+
## Where it is created
|
|
8
|
+
|
|
9
|
+
```svelte
|
|
10
|
+
<script>
|
|
11
|
+
import gsap from "gsap";
|
|
12
|
+
|
|
13
|
+
let root;
|
|
14
|
+
|
|
15
|
+
$effect(() => {
|
|
16
|
+
const ctx = gsap.context(() => {
|
|
17
|
+
gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
|
|
18
|
+
}, root);
|
|
19
|
+
|
|
20
|
+
return () => ctx.revert();
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<section bind:this={root}>…</section>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`$effect` runs after the component is mounted in the browser, and the function
|
|
28
|
+
it returns runs before the next run and when the component is destroyed. In
|
|
29
|
+
Svelte 4, or in a codebase that has not moved to runes, the same body goes in
|
|
30
|
+
`onMount` and returns the same cleanup.
|
|
31
|
+
|
|
32
|
+
Check how the project declares a bound element before copying this: `let root`
|
|
33
|
+
in classic syntax, `let root = $state()` in runes.
|
|
34
|
+
|
|
35
|
+
## Where it is torn down
|
|
36
|
+
|
|
37
|
+
The function returned from `$effect` or from `onMount`, calling `ctx.revert()`.
|
|
38
|
+
That reverts every tween, ScrollTrigger, Observer, Draggable, SplitText and
|
|
39
|
+
matchMedia created inside.
|
|
40
|
+
|
|
41
|
+
An effect re-runs whenever a rune it read changes. If the body creates
|
|
42
|
+
animations, either keep it free of reactive reads, or accept the re-run and let
|
|
43
|
+
the returned cleanup revert the previous one first — which it does, before the
|
|
44
|
+
body runs again.
|
|
45
|
+
|
|
46
|
+
Timers, listeners and observers are yours, in the same cleanup.
|
|
47
|
+
|
|
48
|
+
## Scope
|
|
49
|
+
|
|
50
|
+
`gsap.context(fn, root)` scopes selector strings to the component's element.
|
|
51
|
+
Plugin config is not scoped: `motionPath.path` and `morphSVG.shape` resolve
|
|
52
|
+
against the whole document, so generate an id per instance — from whatever
|
|
53
|
+
unique-id helper your Svelte version provides, or a module counter — rather than
|
|
54
|
+
hardcoding one.
|
|
55
|
+
|
|
56
|
+
## Server rendering and hydration
|
|
57
|
+
|
|
58
|
+
Neither `$effect` nor `onMount` runs during server rendering, so this is safe
|
|
59
|
+
under SvelteKit as written. Do not read `window`, `document` or `matchMedia` at
|
|
60
|
+
the top level of `<script>`: that runs on the server. Read them inside the
|
|
61
|
+
effect.
|
|
62
|
+
|
|
63
|
+
Render the neutral state on the server and correct it after mount, so the
|
|
64
|
+
hydrated markup matches what was sent.
|
|
65
|
+
|
|
66
|
+
## Reaching the element
|
|
67
|
+
|
|
68
|
+
`bind:this` for the root, `data-*` attributes for the parts. Svelte scopes CSS
|
|
69
|
+
classes by compiling them, so a class selector in a tween may not match what is
|
|
70
|
+
in the DOM — a `data-*` attribute is stable and says what it is for.
|
|
71
|
+
|
|
72
|
+
## Every frame
|
|
73
|
+
|
|
74
|
+
Refs, not `$state`. Writing a rune every frame re-renders the component;
|
|
75
|
+
`gsap.quickTo` writes straight to the element. State is for what exists — is the
|
|
76
|
+
drawer open, which tab is active.
|
|
77
|
+
|
|
78
|
+
## Page and route changes
|
|
79
|
+
|
|
80
|
+
SvelteKit destroys the outgoing component, so the context goes with it. For an
|
|
81
|
+
exit animation, put the overlay in a layout, which navigation does not destroy,
|
|
82
|
+
or hold the navigation while a timeline plays with the router's own hooks.
|
|
83
|
+
|
|
84
|
+
After navigation, call `ScrollTrigger.refresh()` once the new page settles.
|
|
85
|
+
|
|
86
|
+
## Failures
|
|
87
|
+
|
|
88
|
+
| Symptom | Cause |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `window is not defined` during a build | `window` read at the top of `<script>` instead of inside the effect |
|
|
91
|
+
| Animations stack up on every state change | an effect that reads a rune, with no cleanup reverting the previous run |
|
|
92
|
+
| Tween targets nothing | a class selector the compiler rewrote; use `data-*` |
|
|
93
|
+
| Hydration mismatch | markup derived from storage or a media query during render |
|
|
94
|
+
|
|
95
|
+
## What the audit covers
|
|
96
|
+
|
|
97
|
+
From 3.2 the audit reads the `<script>` block of `.svelte` files, with offsets
|
|
98
|
+
kept, so reported lines match the file. Every rule that is not React-specific
|
|
99
|
+
applies; `orphan-tween`, `state-per-event` and `shared-plugin-id` do not run
|
|
100
|
+
here.
|
|
101
|
+
|
|
102
|
+
## Versions
|
|
103
|
+
|
|
104
|
+
Verified against `gsap@3.15`. Written for `svelte@5`, with the `onMount` form
|
|
105
|
+
for `svelte@4`.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Three.js
|
|
2
|
+
|
|
3
|
+
GSAP is the **orchestration layer**, not the renderer. Three owns the scene, the
|
|
4
|
+
camera and the draw loop. GSAP owns *when* values change and *how* they ease
|
|
5
|
+
between states. With React Three Fiber, read [r3f.md](r3f.md) instead.
|
|
6
|
+
|
|
7
|
+
Three does not come with GSAP, and most projects have neither. Check
|
|
8
|
+
`package.json` before writing code against it, and say so if it is not there.
|
|
9
|
+
|
|
10
|
+
## The division
|
|
11
|
+
|
|
12
|
+
| GSAP | Three |
|
|
13
|
+
|---|---|
|
|
14
|
+
| timing, easing, sequencing | geometry, materials, lights |
|
|
15
|
+
| camera moves between positions | camera projection, the render loop |
|
|
16
|
+
| object transforms over time | the transforms themselves |
|
|
17
|
+
| material property transitions | shaders |
|
|
18
|
+
| tying it all to scroll | rendering the result |
|
|
19
|
+
|
|
20
|
+
Do not rebuild a physics engine or a particle system in GSAP, and do not
|
|
21
|
+
hand-write easing in the render loop when a tween says it in one line.
|
|
22
|
+
|
|
23
|
+
## Where it is created
|
|
24
|
+
|
|
25
|
+
Three objects are plain JavaScript objects, so GSAP tweens them directly. Nested
|
|
26
|
+
properties are their own targets:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
gsap.to(mesh.position, { x: 2, y: 1, duration: 1.2, ease: "power3.inOut" });
|
|
30
|
+
gsap.to(mesh.rotation, { y: Math.PI * 2, duration: 3, ease: "none", repeat: -1 });
|
|
31
|
+
gsap.to(mesh.material, { opacity: 0, duration: 0.6 });
|
|
32
|
+
gsap.to(mesh.scale, { x: 1.4, y: 1.4, z: 1.4, duration: 0.5 });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`gsap.to(mesh, { position: { x: 2 } })` does not work. Target `mesh.position`.
|
|
36
|
+
|
|
37
|
+
For a material to animate opacity, `transparent` must already be true — setting
|
|
38
|
+
it mid-tween causes a shader recompile.
|
|
39
|
+
|
|
40
|
+
Shader uniforms are `{ value }` objects, which tween cleanly and are the usual
|
|
41
|
+
bridge between a timeline and a custom effect:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
gsap.to(material.uniforms.uProgress, { value: 1, duration: 1.5, ease: "power2.inOut" });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Create all of it inside a `gsap.context`, as in [vanilla.md](vanilla.md): the
|
|
48
|
+
lifecycle is the vanilla one, because Three has no component model of its own.
|
|
49
|
+
|
|
50
|
+
## Where it is torn down
|
|
51
|
+
|
|
52
|
+
Two separate jobs, and GSAP only does the first:
|
|
53
|
+
|
|
54
|
+
- `ctx.revert()` for tweens, timelines and ScrollTriggers.
|
|
55
|
+
- **Three's own disposal is yours**: geometries, materials, textures, render
|
|
56
|
+
targets, and the renderer itself. GSAP's cleanup does not touch them, and
|
|
57
|
+
nothing else will.
|
|
58
|
+
|
|
59
|
+
Stop the render loop too. A `requestAnimationFrame` loop that outlives the
|
|
60
|
+
canvas keeps the GPU busy for a page nobody is looking at.
|
|
61
|
+
|
|
62
|
+
## Scope
|
|
63
|
+
|
|
64
|
+
Selector strings do not apply: targets are object references. The scoping
|
|
65
|
+
question becomes ownership — a module-level scene shared by two mounts animates
|
|
66
|
+
twice. Build the scene per mount, or key the timeline to the instance.
|
|
67
|
+
|
|
68
|
+
## Server rendering and hydration
|
|
69
|
+
|
|
70
|
+
None: WebGL exists only in the browser. Import the module that creates the scene
|
|
71
|
+
dynamically, after mount, so the server never evaluates it and the bundle does
|
|
72
|
+
not carry Three on routes that have no canvas.
|
|
73
|
+
|
|
74
|
+
## Reaching the element
|
|
75
|
+
|
|
76
|
+
By reference, held from where the object was created. Traversing the scene by
|
|
77
|
+
name (`scene.getObjectByName("Cube.003")`) binds the animation to whatever the
|
|
78
|
+
exporter happened to call it, which the next export changes.
|
|
79
|
+
|
|
80
|
+
## Every frame
|
|
81
|
+
|
|
82
|
+
The render loop is already per-frame; GSAP does not need to be. Use a tween for
|
|
83
|
+
transitions between states, and the loop for values that follow the clock. When
|
|
84
|
+
a tween must write something the loop reads, tween a proxy object and read it in
|
|
85
|
+
the loop rather than calling into Three from `onUpdate` many times per frame.
|
|
86
|
+
|
|
87
|
+
The one exception is a camera with `lookAt`, which overwrites the rotation you
|
|
88
|
+
tween. Tween a proxy target and call `lookAt` from `onUpdate`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const look = { x: 0, y: 0, z: 0 };
|
|
92
|
+
gsap.to(look, {
|
|
93
|
+
x: 2,
|
|
94
|
+
duration: 2,
|
|
95
|
+
onUpdate: () => camera.lookAt(look.x, look.y, look.z),
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
For scroll-driven 3D, scrub a timeline rather than computing from `scrollY` in
|
|
100
|
+
the loop, with `ease: "none"` on every child, as with any scrub. See
|
|
101
|
+
[scrolltrigger.md](../reference/scrolltrigger.md).
|
|
102
|
+
|
|
103
|
+
## Page and route changes
|
|
104
|
+
|
|
105
|
+
Dispose on leaving, and rebuild on arriving. A canvas kept across a navigation
|
|
106
|
+
is a live GPU context for a page that is gone.
|
|
107
|
+
|
|
108
|
+
## Failures
|
|
109
|
+
|
|
110
|
+
| Symptom | Cause |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Nothing moves | tweened `mesh` instead of `mesh.position` |
|
|
113
|
+
| Opacity does nothing, or the frame stutters once | `transparent` set during the tween |
|
|
114
|
+
| The GPU stays busy after leaving the page | the render loop or the renderer was never stopped |
|
|
115
|
+
| Memory grows on every visit | geometries, materials or textures never disposed |
|
|
116
|
+
| The camera snaps back every frame | `lookAt` overwriting a tweened rotation |
|
|
117
|
+
| The page is slow on phones before anything moves | Three in the initial bundle instead of imported dynamically |
|
|
118
|
+
|
|
119
|
+
## Cost and accessibility
|
|
120
|
+
|
|
121
|
+
3D is the most expensive thing on a page, and the constraints are harder than
|
|
122
|
+
for DOM animation:
|
|
123
|
+
|
|
124
|
+
- A WebGL canvas runs the GPU continuously. On a phone that is battery and heat.
|
|
125
|
+
- Give the canvas a real fallback — a static image — for low-power devices and
|
|
126
|
+
for `prefers-reduced-motion`. Stopping the animation while leaving a running
|
|
127
|
+
canvas is not a reduced-motion design.
|
|
128
|
+
- Nothing essential — text, navigation, product information — should exist only
|
|
129
|
+
inside the canvas.
|
|
130
|
+
|
|
131
|
+
## What the audit covers
|
|
132
|
+
|
|
133
|
+
Every rule except the React-specific three, on the `.js`, `.ts` and `.mjs`
|
|
134
|
+
modules that build the scene. It cannot see disposal, the render loop or GPU
|
|
135
|
+
cost: those are read by hand, and measured with
|
|
136
|
+
[performance.md](../reference/performance.md).
|
|
137
|
+
|
|
138
|
+
## Versions
|
|
139
|
+
|
|
140
|
+
Verified against `gsap@3.15`. Written for `three@0.186`, whose disposal and
|
|
141
|
+
uniform shapes it shares with earlier versions.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Vanilla JavaScript
|
|
2
|
+
|
|
3
|
+
No framework: you own the mount and the unmount, so the only question is
|
|
4
|
+
whether the element this animates can ever be removed.
|
|
5
|
+
|
|
6
|
+
## Where it is created
|
|
7
|
+
|
|
8
|
+
Inside `gsap.context(fn, root)`, in a function that runs when the element is in
|
|
9
|
+
the document:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import gsap from "gsap";
|
|
13
|
+
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
14
|
+
|
|
15
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
16
|
+
|
|
17
|
+
export function mountReveal(root) {
|
|
18
|
+
const ctx = gsap.context(() => {
|
|
19
|
+
const mm = gsap.matchMedia();
|
|
20
|
+
mm.add(
|
|
21
|
+
{
|
|
22
|
+
motion: "(prefers-reduced-motion: no-preference)",
|
|
23
|
+
reduced: "(prefers-reduced-motion: reduce)",
|
|
24
|
+
},
|
|
25
|
+
({ conditions }) => {
|
|
26
|
+
if (conditions.reduced) {
|
|
27
|
+
gsap.set("[data-item]", { autoAlpha: 1, y: 0 });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
gsap.from("[data-item]", {
|
|
31
|
+
autoAlpha: 0,
|
|
32
|
+
y: 24,
|
|
33
|
+
stagger: { amount: 0.4 },
|
|
34
|
+
scrollTrigger: { trigger: root, start: "top 75%" },
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}, root);
|
|
39
|
+
|
|
40
|
+
return () => ctx.revert();
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The returned function is the whole teardown contract. Hand it to whatever owns
|
|
45
|
+
the element's life.
|
|
46
|
+
|
|
47
|
+
## Where it is torn down
|
|
48
|
+
|
|
49
|
+
`ctx.revert()` undoes every tween, ScrollTrigger, Observer, Draggable, SplitText
|
|
50
|
+
and matchMedia created inside the context, and restores inline styles.
|
|
51
|
+
|
|
52
|
+
Timers, `addEventListener` calls and observers are not GSAP's. Clear them in the
|
|
53
|
+
same cleanup.
|
|
54
|
+
|
|
55
|
+
A static page that never removes the element can skip teardown. Anything that
|
|
56
|
+
swaps content — a client-side router, HTMX, Turbo, a modal, a web component —
|
|
57
|
+
cannot: every swap would otherwise leave the previous run's triggers behind.
|
|
58
|
+
|
|
59
|
+
## Scope
|
|
60
|
+
|
|
61
|
+
The context's second argument is the root. Selector strings inside resolve
|
|
62
|
+
within it, so the same widget mounted twice does not animate the other's
|
|
63
|
+
elements.
|
|
64
|
+
|
|
65
|
+
Plugin config is not scoped: `motionPath.path` and `morphSVG.shape` resolve
|
|
66
|
+
against the whole document. Generate an id per instance rather than hardcoding
|
|
67
|
+
one, in every framework and here too.
|
|
68
|
+
|
|
69
|
+
## Server rendering and hydration
|
|
70
|
+
|
|
71
|
+
There is none unless you add it. If the markup is rendered by a server template,
|
|
72
|
+
the only rule that carries over is that the script runs after the element
|
|
73
|
+
exists — a `defer`red module, or a `DOMContentLoaded` handler.
|
|
74
|
+
|
|
75
|
+
## Reaching the element
|
|
76
|
+
|
|
77
|
+
Pass the root in, and query inside it. A module that reaches for
|
|
78
|
+
`document.querySelector` at import time binds to whatever happened to be on the
|
|
79
|
+
first page, and silently animates nothing after a swap.
|
|
80
|
+
|
|
81
|
+
## Every frame
|
|
82
|
+
|
|
83
|
+
Use `gsap.quickTo` or `gsap.quickSetter` created once, outside the handler, and
|
|
84
|
+
call it from the event. Creating a tween per pointer event allocates one tween
|
|
85
|
+
per frame, each fighting the last.
|
|
86
|
+
|
|
87
|
+
## Page and route changes
|
|
88
|
+
|
|
89
|
+
If a router swaps content, mount on the new page and revert before the swap. The
|
|
90
|
+
library's own events are the hooks: Turbo's `turbo:load` and
|
|
91
|
+
`turbo:before-render`, HTMX's `htmx:afterSwap`, or your router's equivalent.
|
|
92
|
+
Call `ScrollTrigger.refresh()` after the new content settles.
|
|
93
|
+
|
|
94
|
+
## Failures
|
|
95
|
+
|
|
96
|
+
| Symptom | Cause |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Animation stops working after a page swap | mounted once at import, never again |
|
|
99
|
+
| Triggers pile up over a session | no `ctx.revert()` before the swap |
|
|
100
|
+
| Selector matches another instance's element | no root passed to `gsap.context` |
|
|
101
|
+
| Scroll positions drift | no `ScrollTrigger.refresh()` after content changed |
|
|
102
|
+
|
|
103
|
+
## What the audit covers
|
|
104
|
+
|
|
105
|
+
Every rule except the React-specific three: `orphan-tween`, `state-per-event`
|
|
106
|
+
and `shared-plugin-id` apply to React files only. Cost, loops, scrub, listeners,
|
|
107
|
+
unmanaged instances, plugin registration, reduced motion and shipped dev tooling
|
|
108
|
+
are checked here.
|
|
109
|
+
|
|
110
|
+
Keep the animation in a `.js`, `.mjs` or `.ts` module — the files the audit
|
|
111
|
+
reads — rather than in an inline `<script>` in an HTML page, which it does not.
|
|
112
|
+
|
|
113
|
+
## Versions
|
|
114
|
+
|
|
115
|
+
Verified against `gsap@3.15`. No framework version applies.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Vue and Nuxt
|
|
2
|
+
|
|
3
|
+
Composition API with `<script setup>`. Nuxt renders on the server, so everything
|
|
4
|
+
here is written to run only in the browser.
|
|
5
|
+
|
|
6
|
+
## Where it is created
|
|
7
|
+
|
|
8
|
+
In `onMounted`, inside a `gsap.context` rooted at the component's element:
|
|
9
|
+
|
|
10
|
+
```vue
|
|
11
|
+
<script setup>
|
|
12
|
+
import gsap from "gsap";
|
|
13
|
+
import { onMounted, onUnmounted, ref } from "vue";
|
|
14
|
+
|
|
15
|
+
const root = ref(null);
|
|
16
|
+
let ctx;
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
ctx = gsap.context(() => {
|
|
20
|
+
gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
|
|
21
|
+
}, root.value);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
onUnmounted(() => ctx?.revert());
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<section ref="root">…</section>
|
|
29
|
+
</template>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Animation that reacts to state belongs in a `watch` that adds its tweens to the
|
|
33
|
+
same context with `ctx.add(() => { /* … */ })`, so they are reverted with it
|
|
34
|
+
rather than outliving it.
|
|
35
|
+
|
|
36
|
+
## Where it is torn down
|
|
37
|
+
|
|
38
|
+
`onUnmounted`, with `ctx.revert()`. That covers every tween, ScrollTrigger,
|
|
39
|
+
Observer, Draggable, SplitText and matchMedia created inside the context.
|
|
40
|
+
|
|
41
|
+
Timers, listeners and observers are yours, in the same hook. A `watch` stopper
|
|
42
|
+
is yours too if you created it outside `setup`'s own scope.
|
|
43
|
+
|
|
44
|
+
`<KeepAlive>` does not unmount: it deactivates. Use `onActivated` and
|
|
45
|
+
`onDeactivated` to pause and resume, or the animation keeps running in a
|
|
46
|
+
component the visitor cannot see.
|
|
47
|
+
|
|
48
|
+
## Scope
|
|
49
|
+
|
|
50
|
+
`gsap.context(fn, root.value)` scopes selector strings to the component's root,
|
|
51
|
+
so a component used twice on a page does not animate the other's elements.
|
|
52
|
+
|
|
53
|
+
Plugin config is not scoped. `motionPath.path` and `morphSVG.shape` resolve
|
|
54
|
+
against the whole document, so generate an id per instance —
|
|
55
|
+
`useId()` in Vue 3.5 and later, or a module counter — rather than hardcoding
|
|
56
|
+
one.
|
|
57
|
+
|
|
58
|
+
## Server rendering and hydration
|
|
59
|
+
|
|
60
|
+
`onMounted` never runs on the server, so this is safe under Nuxt as written. Two
|
|
61
|
+
rules follow:
|
|
62
|
+
|
|
63
|
+
- Do not read `window`, `document` or `matchMedia` in `setup`'s body — it runs
|
|
64
|
+
on the server. Read them inside `onMounted`.
|
|
65
|
+
- Render the neutral state on the server and correct it after mount, or the
|
|
66
|
+
hydrated markup disagrees with what was sent.
|
|
67
|
+
|
|
68
|
+
In Nuxt, `<ClientOnly>` is a way to skip server rendering for a widget entirely,
|
|
69
|
+
at the cost of rendering nothing until hydration — a poor trade for a section
|
|
70
|
+
that carries content, a reasonable one for a canvas.
|
|
71
|
+
|
|
72
|
+
## Reaching the element
|
|
73
|
+
|
|
74
|
+
A template ref for the root, and `data-*` attributes inside it for the parts.
|
|
75
|
+
Querying by tag or position (`root.value.children[2]`) breaks the next time the
|
|
76
|
+
template changes.
|
|
77
|
+
|
|
78
|
+
A child component's root element is reachable with a ref only when it is a
|
|
79
|
+
single-root component; otherwise pass a class or a `data-*` attribute down and
|
|
80
|
+
query it inside the context.
|
|
81
|
+
|
|
82
|
+
## Every frame
|
|
83
|
+
|
|
84
|
+
Refs, not reactive state. A `ref()` written every frame triggers Vue's
|
|
85
|
+
reactivity and re-renders the component; `gsap.quickTo` writes straight to the
|
|
86
|
+
element. Reactive state is for what exists — is the panel open, which tab is
|
|
87
|
+
active.
|
|
88
|
+
|
|
89
|
+
## Page and route changes
|
|
90
|
+
|
|
91
|
+
Vue Router replaces the view, and `onUnmounted` runs, so the context goes with
|
|
92
|
+
it. For an exit animation, either put the overlay in the layout, which the
|
|
93
|
+
navigation does not unmount, or use the router's own guards to hold the
|
|
94
|
+
navigation while a timeline plays.
|
|
95
|
+
|
|
96
|
+
Call `ScrollTrigger.refresh()` after the new view settles — in Nuxt, after
|
|
97
|
+
`nextTick()` on the route change.
|
|
98
|
+
|
|
99
|
+
## Failures
|
|
100
|
+
|
|
101
|
+
| Symptom | Cause |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `window is not defined` during a Nuxt build | `window` read in `setup` instead of `onMounted` |
|
|
104
|
+
| Hydration mismatch warning | state derived from storage or a media query during render |
|
|
105
|
+
| The animation keeps running after leaving the page | `<KeepAlive>` deactivation treated as unmount |
|
|
106
|
+
| Tweens from a `watch` are never reverted | created outside the context instead of through `ctx.add` |
|
|
107
|
+
| Two instances animate each other | no root passed to `gsap.context` |
|
|
108
|
+
|
|
109
|
+
## What the audit covers
|
|
110
|
+
|
|
111
|
+
From 3.2 the audit reads the `<script>` block of `.vue` files, with offsets
|
|
112
|
+
kept, so reported lines match the file. It checks that a context created in
|
|
113
|
+
`onMounted` is reverted, along with every rule that is not React-specific.
|
|
114
|
+
|
|
115
|
+
`orphan-tween`, `state-per-event` and `shared-plugin-id` are React-only and do
|
|
116
|
+
not run here.
|
|
117
|
+
|
|
118
|
+
## Versions
|
|
119
|
+
|
|
120
|
+
Verified against `gsap@3.15`. Written for `vue@3.5` and `nuxt@4`.
|
|
@@ -177,4 +177,4 @@ project's messages itself could bundle every locale.
|
|
|
177
177
|
## Related
|
|
178
178
|
|
|
179
179
|
[preset/loader.md](../preset/loader.md) · [reference/svg.md](../reference/svg.md)
|
|
180
|
-
· [
|
|
180
|
+
· [adapter/react.md](../adapter/react.md)
|
|
@@ -180,7 +180,7 @@ that context** — `MatchMedia`'s constructor registers with whatever context is
|
|
|
180
180
|
active (verified against gsap 3.15), and `Context.add` makes itself active while your
|
|
181
181
|
function runs. An explicit `mm.revert()` there is redundant, not wrong. Created
|
|
182
182
|
outside one — module scope, a plain `useEffect` — it is yours to revert. See
|
|
183
|
-
[react
|
|
183
|
+
[react.md](../adapter/react.md).
|
|
184
184
|
|
|
185
185
|
## `context`
|
|
186
186
|
|