@mehshekari/gsap-motion 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE.md CHANGED
@@ -3,6 +3,65 @@
3
3
  This skill — its instructions, references, presets, examples, templates and
4
4
  scripts — is released under the MIT License. See [LICENSE](LICENSE).
5
5
 
6
+ ## Third-party code in the audit
7
+
8
+ `scripts/lib/vendor/parser.mjs` bundles two MIT-licensed packages, so the audit
9
+ can read JavaScript and TypeScript without installing anything. It is generated
10
+ from the versions its first lines name.
11
+
12
+ ### acorn
13
+
14
+ ```text
15
+ MIT License
16
+
17
+ Copyright (C) 2012-2022 by various contributors (see AUTHORS)
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
+ THE SOFTWARE.
36
+ ```
37
+
38
+ ### @sveltejs/acorn-typescript
39
+
40
+ ```text
41
+ MIT License
42
+
43
+ Copyright (c) 2022 Tyreal Hu
44
+ Copyright (c) 2025 The Svelte Team
45
+
46
+ Permission is hereby granted, free of charge, to any person obtaining a copy
47
+ of this software and associated documentation files (the "Software"), to deal
48
+ in the Software without restriction, including without limitation the rights
49
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
50
+ copies of the Software, and to permit persons to whom the Software is
51
+ furnished to do so, subject to the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be included in all
54
+ copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
57
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
58
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
59
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
60
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
61
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
62
+ SOFTWARE.
63
+ ```
64
+
6
65
  ## GSAP is licensed separately
7
66
 
8
67
  **GSAP is not part of this skill, and it is not MIT-licensed.** The code this
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@2.1.0 audit src --quiet"
31
+ "lint": "eslint && npx @mehshekari/gsap-motion@3.1.0 audit src --quiet"
32
32
  ```
33
33
 
34
34
  Or install it with `npm install --save-dev --save-exact @mehshekari/gsap-motion`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mehshekari/gsap-motion",
3
- "version": "2.1.0",
3
+ "version": "3.1.0",
4
4
  "description": "Install the gsap-creative-animation Agent Skill, and audit GSAP animation for leaks, jank and accessibility failures — with npx.",
5
5
  "keywords": [
6
6
  "gsap",
package/skill/NOTICE.md CHANGED
@@ -3,6 +3,65 @@
3
3
  This skill — its instructions, references, presets, examples, templates and
4
4
  scripts — is released under the MIT License. See [LICENSE](LICENSE).
5
5
 
6
+ ## Third-party code in the audit
7
+
8
+ `scripts/lib/vendor/parser.mjs` bundles two MIT-licensed packages, so the audit
9
+ can read JavaScript and TypeScript without installing anything. It is generated
10
+ from the versions its first lines name.
11
+
12
+ ### acorn
13
+
14
+ ```text
15
+ MIT License
16
+
17
+ Copyright (C) 2012-2022 by various contributors (see AUTHORS)
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
+ THE SOFTWARE.
36
+ ```
37
+
38
+ ### @sveltejs/acorn-typescript
39
+
40
+ ```text
41
+ MIT License
42
+
43
+ Copyright (c) 2022 Tyreal Hu
44
+ Copyright (c) 2025 The Svelte Team
45
+
46
+ Permission is hereby granted, free of charge, to any person obtaining a copy
47
+ of this software and associated documentation files (the "Software"), to deal
48
+ in the Software without restriction, including without limitation the rights
49
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
50
+ copies of the Software, and to permit persons to whom the Software is
51
+ furnished to do so, subject to the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be included in all
54
+ copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
57
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
58
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
59
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
60
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
61
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
62
+ SOFTWARE.
63
+ ```
64
+
6
65
  ## GSAP is licensed separately
7
66
 
8
67
  **GSAP is not part of this skill, and it is not MIT-licensed.** The code this
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: "2.1.0"
7
+ version: "3.1.0"
8
8
  verified-gsap: "3.15"
9
9
  verified-gsap-react: "2.1"
10
10
  ---
@@ -160,8 +160,9 @@ Both scripts catch failures that are **silent**. Run them from the project
160
160
  root; `<skill-dir>` is where this skill is installed.
161
161
 
162
162
  ```bash
163
- # Leaks, per-event tweens, layout properties, eased loops, shared plugin ids,
164
- # unregistered plugins, dev tooling left in. Exit 1 on an error.
163
+ # Leaks, per-event tweens, layout properties, eased loops, an onComplete that
164
+ # never fires, shared plugin ids, unregistered plugins, dev tooling left in.
165
+ # Exit 1 on an error.
165
166
  node <skill-dir>/scripts/audit-gsap.mjs [path...] # default: src
166
167
  node <skill-dir>/scripts/audit-gsap.mjs src --quiet # errors only
167
168
  node <skill-dir>/scripts/audit-gsap.mjs src --json # machine-readable
@@ -119,10 +119,15 @@ tl.addLabel("hold")
119
119
  ```
120
120
 
121
121
  Get this wrong and nothing visibly breaks: the ceiling still lifts the curtain,
122
- just late, on every visit. That is exactly why it is worth checking.
123
-
124
- **The page renders underneath the whole time.** The curtain only covers. A
125
- JavaScript failure leaves the visitor on the site, not behind a curtain.
122
+ just late, on every visit. That is exactly why it is worth checking, and why
123
+ `audit-gsap` reports it as an error, `never-completes`.
124
+
125
+ **The page renders underneath, and the curtain has a CSS exit.** The curtain
126
+ only covers. But it is in the server-rendered HTML, so a visitor whose
127
+ JavaScript never runs would get the curtain and nothing able to remove it — the
128
+ ceiling is JavaScript too. A CSS animation that hides it a little after the
129
+ ceiling is the exit that needs no script; see
130
+ [cinematic.md](../preset/cinematic.md#without-javascript).
126
131
 
127
132
  **Session gating in the layout effect, behind storage's real failures.**
128
133
  Storage cannot be read during render without the server and client
@@ -108,7 +108,39 @@ return () => window.clearTimeout(ceiling);
108
108
 
109
109
  Keep the total under ~3s. Play it once per session, not once per route — an
110
110
  intro on every navigation is an obstacle. Render the page underneath the whole
111
- time so a JavaScript failure cannot trap the visitor behind a curtain.
111
+ time, so the curtain only ever covers.
112
+
113
+ ## Without JavaScript
114
+
115
+ Rendering the page underneath is necessary, and not enough. A curtain rendered
116
+ on the server — which a client component is, too — is in the HTML, so a visitor
117
+ whose JavaScript never runs gets the curtain and nothing able to remove it: a
118
+ blocked script, a chunk that fails to load after a deploy, a hydration error.
119
+ The ceiling above is JavaScript as well.
120
+
121
+ Give the curtain an exit that needs no script — a CSS animation that hides it a
122
+ little after the JavaScript ceiling:
123
+
124
+ ```css
125
+ @keyframes curtain-exit {
126
+ to { opacity: 0; visibility: hidden; }
127
+ }
128
+
129
+ [data-curtain] {
130
+ animation: curtain-exit 0.3s ease-out 5s forwards;
131
+ }
132
+ ```
133
+
134
+ - **It outranks GSAP.** Animations beat inline styles in the cascade, so once it
135
+ has run, an `autoAlpha` tween cannot bring the curtain back — verified in
136
+ Chrome. On the normal path the curtain is gone long before the delay ends.
137
+ - **The delay counts from the HTML, not from hydration.** Set it beyond the
138
+ JavaScript ceiling plus a slow hydration. At worst, a page that took that
139
+ long to become interactive plays its intro under an invisible curtain.
140
+ - **`visibility: hidden` is the part that matters.** It removes the curtain from
141
+ hit-testing, so the page is clickable, not just visible.
142
+ - A `<noscript>` style only covers JavaScript that is switched off, not
143
+ JavaScript that loaded and failed. Use the animation.
112
144
 
113
145
  ## Reduced motion
114
146
 
@@ -116,10 +116,13 @@ return () => window.clearTimeout(ceiling);
116
116
 
117
117
  A timeline containing an infinitely repeating child **never completes**, so its
118
118
  `onComplete` never fires. Give the timeline a finite beat of its own to end on,
119
- or drive the handover from elsewhere.
119
+ or drive the handover from elsewhere. `audit-gsap` reports it as
120
+ `never-completes`.
120
121
 
121
- Render the real page underneath the whole time, so a visitor whose JavaScript
122
- fails gets the site rather than a permanent curtain.
122
+ Render the real page underneath the whole time. That alone does not save a
123
+ visitor whose JavaScript never runs: a server-rendered curtain is then all they
124
+ get, and the ceiling is JavaScript too. Give the curtain a CSS exit as well —
125
+ see [cinematic.md](cinematic.md#without-javascript).
123
126
 
124
127
  ## Exit
125
128
 
@@ -69,6 +69,11 @@ rather than fighting over `transform`:
69
69
  transformOrigin: "50% 100%" }
70
70
  ```
71
71
 
72
+ **On SVG, the origin comes with the first transform.** One that arrives after
73
+ the element is already scaled, rotated or skewed — in a `fromTo`'s to-vars —
74
+ leaves it offset, silently. Put it in the from-vars; see
75
+ [svg.md](svg.md#transform-origin).
76
+
72
77
  `xPercent`/`yPercent` are percentages of the element's own size, which is what
73
78
  centres something without knowing its dimensions.
74
79
 
@@ -117,9 +122,6 @@ gsap.utils.selector(ref); // scoped selector function
117
122
  gsap.utils.shuffle(items);
118
123
  ```
119
124
 
120
- `mapRange` and `wrap` are the two that most often replace ten lines of
121
- hand-rolled maths in a scroll or marquee.
122
-
123
125
  ## `quickTo` and `quickSetter`
124
126
 
125
127
  For values driven by a high-frequency event — pointer move, scroll, rAF —
@@ -228,6 +230,3 @@ question — see [motion-design.md](motion-design.md).
228
230
  CustomEase.create("brand", "M0,0 C0.16,1 0.3,1 1,1");
229
231
  gsap.to(el, { y: 0, ease: "brand" });
230
232
  ```
231
-
232
- `CustomWiggle` and `CustomBounce` register named eases the same way, for shake
233
- and impact without hand-built keyframes.
@@ -168,6 +168,38 @@ rendered. `ease: "none"` on anything looping.
168
168
  To author a path by eye, use `MotionPathHelper` in development only and bake the
169
169
  `d` it gives you. It is an editor — never ship it.
170
170
 
171
+ ## Transform origin
172
+
173
+ Set `transformOrigin` — or `svgOrigin` — no later than the first scale, rotation
174
+ or skew. When the origin of an SVG element that is already transformed changes,
175
+ GSAP adds a translate that holds the element where it is (`smoothOrigin`), and
176
+ that translate stays after the tween.
177
+
178
+ A `fromTo` renders its from-vars first, so this is the shape that breaks:
179
+
180
+ ```ts
181
+ // ✗ ends a whole radius up and left of where it should be
182
+ gsap.fromTo(dot, { scale: 0 }, { scale: 1, transformOrigin: "50% 50%" });
183
+
184
+ // ✓
185
+ gsap.fromTo(dot, { scale: 0, transformOrigin: "50% 50%" }, { scale: 1 });
186
+ ```
187
+
188
+ Measured in gsap 3.15 in Chrome, with the origin only in the to-vars:
189
+
190
+ | From-vars | The element ends |
191
+ |---|---|
192
+ | `scale: 0` | offset by the origin's distance from its own top-left corner |
193
+ | `scale: 0.94` | offset by that distance × 0.06 — a few pixels, easy to miss |
194
+ | `rotation: 90`, `skewX: 20` | offset too |
195
+ | `x: 20`, `scale: 1` | where it should — a translation or an identity start is safe |
196
+
197
+ A `gsap.set(el, { scale: 0 })` followed by a `to` that brings the origin does the
198
+ same. HTML elements are unaffected, and a mask or clip path makes no difference.
199
+ Any one of these fixes it: the origin in the from-vars, a `gsap.set` of the
200
+ origin before the tween, or `smoothOrigin: false`. `audit-gsap` reports the
201
+ `fromTo` shape as `late-transform-origin`.
202
+
171
203
  ## Masks and clip paths
172
204
 
173
205
  Often better than either plugin, and cheaper. A mask reveal is a transform on a
@@ -184,8 +216,8 @@ rect inside a `<mask>`:
184
216
  gsap.from("[data-wipe]", { scaleX: 0, transformOrigin: "0% 50%", duration: 0.8 });
185
217
  ```
186
218
 
187
- Greyscale in a mask is an alpha stop, not a colour — the one literal this
188
- repo's colour rule allows.
219
+ Greyscale in a mask is an alpha stop, not a colour — the one literal a
220
+ token-only colour rule has no reason to refuse.
189
221
 
190
222
  ## Choosing
191
223
 
@@ -86,7 +86,8 @@ tl.from("[data-mark]", { ... }) // plays once
86
86
  `repeat: -1` on the timeline replays the entrance too. And a timeline containing
87
87
  an infinitely repeating child **never completes**, so its `onComplete` never
88
88
  fires — if something downstream waits on that, give the timeline a finite beat
89
- of its own to end on, or drive the handover from elsewhere.
89
+ of its own to end on, or drive the handover from elsewhere. `audit-gsap`
90
+ reports it as `never-completes`.
90
91
 
91
92
  ## Paused timelines as state
92
93
 
@@ -13,10 +13,12 @@
13
13
  * Rhythm, easing choice and whether an animation earns its place are not
14
14
  * checkable and are not attempted — reference/motion-design.md owns those.
15
15
  *
16
- * This is static analysis without a parser, so it is deliberately conservative
17
- * and will still be wrong sometimes. `info` findings have the loosest match.
18
- * A wrong finding is a bug in this script; report it rather than working
19
- * around it.
16
+ * It reads each file's syntax tree — with a vendored parser, so nothing is
17
+ * installed — and follows what can be followed inside one file: aliases, a
18
+ * helper called only from a context, a timeline kept under a name. It does not
19
+ * follow calls into other files. A file it cannot parse is reported as
20
+ * `not-parsed`, never passed as clean. A wrong finding is a bug in this script;
21
+ * report it rather than working around it.
20
22
  *
21
23
  * Usage — paths resolve from the current directory, so run it from the project
22
24
  * root, wherever the skill itself is installed:
@@ -29,7 +31,7 @@
29
31
  import { existsSync } from "node:fs";
30
32
  import { resolve } from "node:path";
31
33
 
32
- import { RULES } from "./lib/rules.mjs";
34
+ import { pluginRegistrations, RULES } from "./lib/rules.mjs";
33
35
  import { collect, lineAt, load } from "./lib/source.mjs";
34
36
 
35
37
  /**
@@ -82,6 +84,7 @@ function waived(source, line, ruleId) {
82
84
  // --- Run ---------------------------------------------------------------------
83
85
 
84
86
  const findings = [];
87
+ const sources = [];
85
88
 
86
89
  for (const path of paths) {
87
90
  for (const file of collect(path)) {
@@ -93,20 +96,54 @@ for (const path of paths) {
93
96
  */
94
97
  if (!source.usesGsap && !/\bgsap\./.test(source.code)) continue;
95
98
 
96
- for (const rule of RULES) {
97
- for (const hit of rule.test(source)) {
98
- const line = lineAt(source.raw, hit.index);
99
- if (waived(source.raw, line, rule.id)) continue;
100
-
99
+ /**
100
+ * What was not checked is reported as not checked. A file that uses GSAP
101
+ * and does not parse gets no rule's verdict — a silent skip would read as
102
+ * clean.
103
+ */
104
+ if (source.parseError) {
105
+ const { line, message } = source.parseError;
106
+ if (!waived(source.raw, line, "not-parsed")) {
101
107
  findings.push({
102
- rule: rule.id,
103
- level: rule.level,
108
+ rule: "not-parsed",
109
+ level: "info",
104
110
  file: source.display,
105
111
  line,
106
- message: hit.message,
107
- hint: hit.hint,
112
+ message: `Not checked: the file could not be parsed (${message}).`,
113
+ hint: "No rule ran on this file. If it is valid JavaScript or TypeScript, the audit's parser is wrong — report it with this line.",
108
114
  });
109
115
  }
116
+ continue;
117
+ }
118
+
119
+ sources.push(source);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Plugin registration is global: `gsap.registerPlugin(ScrollTrigger)` in one
125
+ * module registers it for every other. What the audited files register between
126
+ * them is collected before any rule runs, so a section that imports a plugin
127
+ * the app's entry registers is not reported as unregistered.
128
+ */
129
+ const registered = new Set(sources.flatMap((source) => [...pluginRegistrations(source)]));
130
+
131
+ for (const source of sources) {
132
+ source.registeredElsewhere = registered;
133
+
134
+ for (const rule of RULES) {
135
+ for (const hit of rule.test(source)) {
136
+ const line = lineAt(source.raw, hit.index);
137
+ if (waived(source.raw, line, rule.id)) continue;
138
+
139
+ findings.push({
140
+ rule: rule.id,
141
+ level: rule.level,
142
+ file: source.display,
143
+ line,
144
+ message: hit.message,
145
+ hint: hit.hint,
146
+ });
110
147
  }
111
148
  }
112
149
  }