svg-scroll-draw 2.13.0 → 2.14.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 CHANGED
@@ -363,7 +363,7 @@ import { ScrollDraw } from 'svg-scroll-draw/vue';
363
363
  ## AOS drop-in — switch from AOS in one line
364
364
 
365
365
  Already using [AOS](https://github.com/michalsnik/aos)? `svg-scroll-draw/aos` speaks its API and
366
- reads its markup, so the move is an import change. Keep every `data-aos`, `data-aos-offset`,
366
+ reads its markup, so the move is an import change — or one command: `npx svg-scroll-draw migrate`. Keep every `data-aos`, `data-aos-offset`,
367
367
  `data-aos-delay`, `data-aos-anchor-placement` and custom animation; delete `aos.css` — the styles ship
368
368
  inside the module. 3.0 KB gzipped with the styles, against 6.7 KB for AOS 2.3.4 with its stylesheet.
369
369
 
@@ -403,7 +403,8 @@ Differences, on purpose:
403
403
 
404
404
  On [ScrollReveal](https://scrollrevealjs.org)? It is GPL-3.0 (with a paid licence for commercial work)
405
405
  and has not shipped since 4.0.9. `svg-scroll-draw/scrollreveal` is an independent, MIT-licensed
406
- implementation of the same API, so the move is an import change. 4.5 KB gzipped, against 5.6 KB.
406
+ implementation of the same API, so the move is an import change — or one command: `npx svg-scroll-draw migrate`.
407
+ 4.5 KB gzipped, against 5.6 KB.
407
408
 
408
409
  ```diff
409
410
  - import ScrollReveal from 'scrollreveal';
@@ -434,8 +435,8 @@ Differences, on purpose:
434
435
  ## vivus drop-in — keep your `new Vivus()` code
435
436
 
436
437
  On [vivus](https://github.com/maxwellito/vivus)? It has not shipped since 0.4.6 (April 2021).
437
- `svg-scroll-draw/vivus` runs the same API, so the move is an import change. 3.9 KB gzipped, against
438
- 4.1 KB.
438
+ `svg-scroll-draw/vivus` runs the same API, so the move is an import change — or one command:
439
+ `npx svg-scroll-draw migrate`. 3.9 KB gzipped, against 4.1 KB.
439
440
 
440
441
  ```diff
441
442
  - import Vivus from 'vivus';
@@ -604,6 +605,19 @@ npx svg-scroll-draw init
604
605
 
605
606
  Scaffolds a ready-to-use starter file for React, Vue, Svelte, Solid, or Vanilla JS.
606
607
 
608
+ ### Switching from AOS, ScrollReveal or vivus
609
+
610
+ ```bash
611
+ npx svg-scroll-draw migrate --dry-run # see what would change
612
+ npx svg-scroll-draw migrate # apply it
613
+ ```
614
+
615
+ Points every `import` / `require` of `aos`, `scrollreveal` and `vivus` at the matching drop-in, swaps CDN
616
+ `<script>` tags for the script-tag builds (dropping any stale `integrity` hash), removes AOS's `aos.css`
617
+ imports, `<link>`s and `@import`s, and swaps the dependency in `package.json`. It rewrites module names,
618
+ URLs and dependency entries only — never the code that uses them — skips `node_modules` and build output,
619
+ and will not write over uncommitted git changes unless you pass `--force`.
620
+
607
621
  ---
608
622
 
609
623
  ## Browser support
@@ -0,0 +1,53 @@
1
+ type AOSAnchorPlacement = 'top-bottom' | 'top-center' | 'top-top' | 'center-bottom' | 'center-center' | 'center-top' | 'bottom-bottom' | 'bottom-center' | 'bottom-top';
2
+ interface AOSOptions {
3
+ /** Px from the trigger point before the animation starts. Default 120. */
4
+ offset?: number;
5
+ /** Ms to wait before animating in. Default 0. */
6
+ delay?: number;
7
+ /** Animation length in ms. Default 400. */
8
+ duration?: number;
9
+ /** An AOS easing name (`'ease-out-back'`…) or any CSS timing function. Default `'ease'`. */
10
+ easing?: string;
11
+ /** Animate only the first time the element scrolls in. Default false. */
12
+ once?: boolean;
13
+ /** Animate out again when the element scrolls past the top. Default false. */
14
+ mirror?: boolean;
15
+ /** Which edge of the element meets which edge of the window. Default `'top-bottom'`. */
16
+ anchorPlacement?: AOSAnchorPlacement;
17
+ /** Skip animations entirely: a boolean, a predicate, or a viewport class. */
18
+ disable?: boolean | 'phone' | 'tablet' | 'mobile' | (() => boolean);
19
+ /** Document event that starts AOS. Default `'DOMContentLoaded'`. */
20
+ startEvent?: string;
21
+ /** Do not watch the DOM for new `[data-aos]` elements. Default false. */
22
+ disableMutationObserver?: boolean;
23
+ /** Class added at init. Default `'aos-init'`. */
24
+ initClassName?: string;
25
+ /** Class added while animated in. Default `'aos-animate'`. */
26
+ animatedClassName?: string;
27
+ /** Also add the `data-aos` value as classes (for animate.css-style sets). Default false. */
28
+ useClassNames?: boolean;
29
+ /** Accepted for compatibility; scroll handling is batched per frame instead. */
30
+ throttleDelay?: number;
31
+ /** Accepted for compatibility; resize handling is debounced internally. */
32
+ debounceDelay?: number;
33
+ }
34
+ /** Start animating every `[data-aos]` element. Same options as AOS. */
35
+ declare function init(options?: AOSOptions): HTMLElement[];
36
+ /** Recalculate every trigger position (after a layout change). */
37
+ declare function refresh(): void;
38
+ /** Find `[data-aos]` elements again (after adding or removing some), then refresh. */
39
+ declare function refreshHard(): void;
40
+ /**
41
+ * Stop listening and remove the injected stylesheet. Not in AOS — added for
42
+ * single-page apps that unmount the page AOS was started on. Elements keep
43
+ * whatever classes they have.
44
+ */
45
+ declare function destroy(): void;
46
+ declare const AOS: {
47
+ init: typeof init;
48
+ refresh: typeof refresh;
49
+ refreshHard: typeof refreshHard;
50
+ destroy: typeof destroy;
51
+ };
52
+
53
+ export { type AOSAnchorPlacement, type AOSOptions, AOS as default, destroy, init, refresh, refreshHard };
@@ -0,0 +1,53 @@
1
+ type AOSAnchorPlacement = 'top-bottom' | 'top-center' | 'top-top' | 'center-bottom' | 'center-center' | 'center-top' | 'bottom-bottom' | 'bottom-center' | 'bottom-top';
2
+ interface AOSOptions {
3
+ /** Px from the trigger point before the animation starts. Default 120. */
4
+ offset?: number;
5
+ /** Ms to wait before animating in. Default 0. */
6
+ delay?: number;
7
+ /** Animation length in ms. Default 400. */
8
+ duration?: number;
9
+ /** An AOS easing name (`'ease-out-back'`…) or any CSS timing function. Default `'ease'`. */
10
+ easing?: string;
11
+ /** Animate only the first time the element scrolls in. Default false. */
12
+ once?: boolean;
13
+ /** Animate out again when the element scrolls past the top. Default false. */
14
+ mirror?: boolean;
15
+ /** Which edge of the element meets which edge of the window. Default `'top-bottom'`. */
16
+ anchorPlacement?: AOSAnchorPlacement;
17
+ /** Skip animations entirely: a boolean, a predicate, or a viewport class. */
18
+ disable?: boolean | 'phone' | 'tablet' | 'mobile' | (() => boolean);
19
+ /** Document event that starts AOS. Default `'DOMContentLoaded'`. */
20
+ startEvent?: string;
21
+ /** Do not watch the DOM for new `[data-aos]` elements. Default false. */
22
+ disableMutationObserver?: boolean;
23
+ /** Class added at init. Default `'aos-init'`. */
24
+ initClassName?: string;
25
+ /** Class added while animated in. Default `'aos-animate'`. */
26
+ animatedClassName?: string;
27
+ /** Also add the `data-aos` value as classes (for animate.css-style sets). Default false. */
28
+ useClassNames?: boolean;
29
+ /** Accepted for compatibility; scroll handling is batched per frame instead. */
30
+ throttleDelay?: number;
31
+ /** Accepted for compatibility; resize handling is debounced internally. */
32
+ debounceDelay?: number;
33
+ }
34
+ /** Start animating every `[data-aos]` element. Same options as AOS. */
35
+ declare function init(options?: AOSOptions): HTMLElement[];
36
+ /** Recalculate every trigger position (after a layout change). */
37
+ declare function refresh(): void;
38
+ /** Find `[data-aos]` elements again (after adding or removing some), then refresh. */
39
+ declare function refreshHard(): void;
40
+ /**
41
+ * Stop listening and remove the injected stylesheet. Not in AOS — added for
42
+ * single-page apps that unmount the page AOS was started on. Elements keep
43
+ * whatever classes they have.
44
+ */
45
+ declare function destroy(): void;
46
+ declare const AOS: {
47
+ init: typeof init;
48
+ refresh: typeof refresh;
49
+ refreshHard: typeof refreshHard;
50
+ destroy: typeof destroy;
51
+ };
52
+
53
+ export { type AOSAnchorPlacement, type AOSOptions, AOS as default, destroy, init, refresh, refreshHard };
@@ -0,0 +1,72 @@
1
+ type StoryEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring' | 'bounce' | 'elastic';
2
+ /** A traced path that strokes itself on across a scroll range. */
3
+ interface DrawAnimation {
4
+ type: 'draw';
5
+ /** DOM id the runtime binds to (with leading '#'), e.g. "#path-3". */
6
+ target: string;
7
+ /** SVG path data, in canvas coordinates. */
8
+ d: string;
9
+ /** Pre-measured total path length (px). */
10
+ length: number;
11
+ /** Scroll progress where the draw begins, e.g. "20%". */
12
+ start: string;
13
+ /** Scroll progress where the draw completes, e.g. "50%". */
14
+ end: string;
15
+ stroke: string;
16
+ strokeWidth: number;
17
+ easing: StoryEasing;
18
+ }
19
+ /** Any layer fading opacity across a scroll range. */
20
+ interface FadeAnimation {
21
+ type: 'fade';
22
+ target: string;
23
+ start: string;
24
+ end: string;
25
+ from: number;
26
+ to: number;
27
+ }
28
+ type StoryAnimation = DrawAnimation | FadeAnimation;
29
+ interface StoryScene {
30
+ id: string;
31
+ /** Optional background layer (a product photo) as a data URL or remote URL. */
32
+ background?: string;
33
+ animations: StoryAnimation[];
34
+ }
35
+ interface Story {
36
+ version: 1;
37
+ /** Total scroll height of the experience, e.g. "400vh". */
38
+ totalHeight: string;
39
+ /** The authoring viewBox the coordinates are expressed in. */
40
+ canvas: {
41
+ width: number;
42
+ height: number;
43
+ };
44
+ scenes: StoryScene[];
45
+ }
46
+
47
+ interface CinematicOptions {
48
+ /** Mount point — a selector or element. Becomes the scroll wrapper. */
49
+ wrapper: string | HTMLElement;
50
+ }
51
+ interface CinematicInstance {
52
+ /** Stop the scroll loop and detach observers (built DOM is left in place). */
53
+ destroy: () => void;
54
+ /** Current global scroll progress through the story (0–1). */
55
+ getProgress: () => number;
56
+ }
57
+ /**
58
+ * The viral loader. Reads a Cinematic Story (authored in the Studio) and wires
59
+ * a scroll-scrubbed timeline: paths stroke themselves on and layers fade in as
60
+ * the user scrolls, all driven off the wrapper's scroll progress.
61
+ *
62
+ * import { Cinematic } from "svg-scroll-draw";
63
+ * import story from "./story.json";
64
+ * new Cinematic({ wrapper: "#app" }).loadStory(story);
65
+ */
66
+ declare class Cinematic {
67
+ private mount;
68
+ constructor(options: CinematicOptions);
69
+ loadStory(story: Story): CinematicInstance;
70
+ }
71
+
72
+ export { Cinematic, type CinematicInstance, type CinematicOptions, type DrawAnimation, type FadeAnimation, type Story, type StoryAnimation, type StoryEasing, type StoryScene };
@@ -0,0 +1,72 @@
1
+ type StoryEasing = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring' | 'bounce' | 'elastic';
2
+ /** A traced path that strokes itself on across a scroll range. */
3
+ interface DrawAnimation {
4
+ type: 'draw';
5
+ /** DOM id the runtime binds to (with leading '#'), e.g. "#path-3". */
6
+ target: string;
7
+ /** SVG path data, in canvas coordinates. */
8
+ d: string;
9
+ /** Pre-measured total path length (px). */
10
+ length: number;
11
+ /** Scroll progress where the draw begins, e.g. "20%". */
12
+ start: string;
13
+ /** Scroll progress where the draw completes, e.g. "50%". */
14
+ end: string;
15
+ stroke: string;
16
+ strokeWidth: number;
17
+ easing: StoryEasing;
18
+ }
19
+ /** Any layer fading opacity across a scroll range. */
20
+ interface FadeAnimation {
21
+ type: 'fade';
22
+ target: string;
23
+ start: string;
24
+ end: string;
25
+ from: number;
26
+ to: number;
27
+ }
28
+ type StoryAnimation = DrawAnimation | FadeAnimation;
29
+ interface StoryScene {
30
+ id: string;
31
+ /** Optional background layer (a product photo) as a data URL or remote URL. */
32
+ background?: string;
33
+ animations: StoryAnimation[];
34
+ }
35
+ interface Story {
36
+ version: 1;
37
+ /** Total scroll height of the experience, e.g. "400vh". */
38
+ totalHeight: string;
39
+ /** The authoring viewBox the coordinates are expressed in. */
40
+ canvas: {
41
+ width: number;
42
+ height: number;
43
+ };
44
+ scenes: StoryScene[];
45
+ }
46
+
47
+ interface CinematicOptions {
48
+ /** Mount point — a selector or element. Becomes the scroll wrapper. */
49
+ wrapper: string | HTMLElement;
50
+ }
51
+ interface CinematicInstance {
52
+ /** Stop the scroll loop and detach observers (built DOM is left in place). */
53
+ destroy: () => void;
54
+ /** Current global scroll progress through the story (0–1). */
55
+ getProgress: () => number;
56
+ }
57
+ /**
58
+ * The viral loader. Reads a Cinematic Story (authored in the Studio) and wires
59
+ * a scroll-scrubbed timeline: paths stroke themselves on and layers fade in as
60
+ * the user scrolls, all driven off the wrapper's scroll progress.
61
+ *
62
+ * import { Cinematic } from "svg-scroll-draw";
63
+ * import story from "./story.json";
64
+ * new Cinematic({ wrapper: "#app" }).loadStory(story);
65
+ */
66
+ declare class Cinematic {
67
+ private mount;
68
+ constructor(options: CinematicOptions);
69
+ loadStory(story: Story): CinematicInstance;
70
+ }
71
+
72
+ export { Cinematic, type CinematicInstance, type CinematicOptions, type DrawAnimation, type FadeAnimation, type Story, type StoryAnimation, type StoryEasing, type StoryScene };
package/dist/cli/init.js CHANGED
@@ -25,8 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli/init.ts
27
27
  var readline = __toESM(require("readline"));
28
- var fs = __toESM(require("fs"));
29
- var path = __toESM(require("path"));
28
+ var fs2 = __toESM(require("fs"));
29
+ var path2 = __toESM(require("path"));
30
30
 
31
31
  // src/cli/generators.ts
32
32
  var SAMPLE_SVG_JSX = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100">
@@ -138,6 +138,254 @@ function needsSelector(framework) {
138
138
  return framework === "vanilla";
139
139
  }
140
140
 
141
+ // src/cli/migrate.ts
142
+ var fs = __toESM(require("fs"));
143
+ var path = __toESM(require("path"));
144
+ var import_node_child_process = require("child_process");
145
+ var LIBS = ["aos", "scrollreveal", "vivus"];
146
+ var LABEL = { aos: "AOS", scrollreveal: "ScrollReveal", vivus: "vivus" };
147
+ var esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
148
+ function rewriteSpecifiers(src, changes) {
149
+ let out = src;
150
+ for (const lib of LIBS) {
151
+ const spec = lib === "aos" ? `aos(?:\\/dist\\/aos(?:\\.js)?)?` : esc(lib);
152
+ const target = `svg-scroll-draw/${lib}`;
153
+ const fromRe = new RegExp(`(\\bfrom\\s*)(['"])${spec}\\2`, "g");
154
+ const dynRe = new RegExp(`(\\bimport\\s*\\(\\s*)(['"])${spec}\\2(\\s*\\))`, "g");
155
+ const reqRe = new RegExp(`(\\brequire\\s*\\(\\s*)(['"])${spec}\\2(\\s*\\))`, "g");
156
+ let n = 0;
157
+ out = out.replace(fromRe, (_m, pre, q) => {
158
+ n++;
159
+ return `${pre}${q}${target}${q}`;
160
+ });
161
+ out = out.replace(dynRe, (_m, pre, q, post) => {
162
+ n++;
163
+ return `${pre}${q}${target}${q}${post}`;
164
+ });
165
+ out = out.replace(reqRe, (_m, pre, q, post) => {
166
+ n++;
167
+ return lib === "aos" ? `${pre}${q}${target}${q}${post}` : `${pre}${q}${target}${q}${post}.default`;
168
+ });
169
+ if (n) changes.push(`${LABEL[lib]}: ${n} import${n === 1 ? "" : "s"} \u2192 ${target}`);
170
+ }
171
+ return out;
172
+ }
173
+ function dropAosCssImports(src, changes) {
174
+ let n = 0;
175
+ const out = src.replace(/^[ \t]*import\s+(['"])aos\/dist\/aos(?:\.min)?\.css\1;?[ \t]*\r?\n?/gm, () => {
176
+ n++;
177
+ return "";
178
+ }).replace(/^[ \t]*require\s*\(\s*(['"])aos\/dist\/aos(?:\.min)?\.css\1\s*\);?[ \t]*\r?\n?/gm, () => {
179
+ n++;
180
+ return "";
181
+ });
182
+ if (n) changes.push(`AOS: removed ${n} aos.css import${n === 1 ? "" : "s"} (the drop-in ships its styles)`);
183
+ return out;
184
+ }
185
+ var SCRIPT_URL = {
186
+ aos: /^(?:https?:)?\/\/(?:unpkg\.com\/aos(?:@[^/]+)?(?:\/dist\/aos(?:\.min)?\.js)?|cdn\.jsdelivr\.net\/npm\/aos(?:@[^/]+)?(?:\/dist\/aos(?:\.min)?\.js)?|cdnjs\.cloudflare\.com\/ajax\/libs\/aos\/[^/]+\/aos(?:\.min)?\.js)$|(?:^|\/)node_modules\/aos\/dist\/aos(?:\.min)?\.js$/,
187
+ scrollreveal: /^(?:https?:)?\/\/(?:unpkg\.com\/scrollreveal(?:@[^/]+)?(?:\/dist\/scrollreveal(?:\.min)?\.js)?|cdn\.jsdelivr\.net\/npm\/scrollreveal(?:@[^/]+)?(?:\/dist\/scrollreveal(?:\.min)?\.js)?|cdnjs\.cloudflare\.com\/ajax\/libs\/scrollReveal\.js\/[^/]+\/scrollreveal(?:\.min)?\.js)$|(?:^|\/)node_modules\/scrollreveal\/dist\/scrollreveal(?:\.min)?\.js$/i,
188
+ vivus: /^(?:https?:)?\/\/(?:unpkg\.com\/vivus(?:@[^/]+)?(?:\/dist\/vivus(?:\.min)?\.js)?|cdn\.jsdelivr\.net\/npm\/vivus(?:@[^/]+)?(?:\/dist\/vivus(?:\.min)?\.js)?|cdnjs\.cloudflare\.com\/ajax\/libs\/vivus\/[^/]+\/vivus(?:\.min)?\.js)$|(?:^|\/)node_modules\/vivus\/dist\/vivus(?:\.min)?\.js$/
189
+ };
190
+ var AOS_CSS_URL = /^(?:https?:)?\/\/(?:unpkg\.com\/aos(?:@[^/]+)?\/dist\/aos\.css|cdn\.jsdelivr\.net\/npm\/aos(?:@[^/]+)?\/dist\/aos\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/aos\/[^/]+\/aos(?:\.min)?\.css)$|(?:^|\/)node_modules\/aos\/dist\/aos\.css$/;
191
+ function rewriteScriptTags(src, version, changes) {
192
+ return src.replace(/<script\b([^>]*)>/gi, (tag, attrs) => {
193
+ const m = /\bsrc\s*=\s*(["'])([^"']+)\1/i.exec(attrs);
194
+ if (!m) return tag;
195
+ const lib = LIBS.find((l) => SCRIPT_URL[l].test(m[2]));
196
+ if (!lib) return tag;
197
+ const url = `https://unpkg.com/svg-scroll-draw@${version}/dist/cdn/${lib}.global.js`;
198
+ const hadIntegrity = /\bintegrity\s*=/i.test(attrs);
199
+ const next = attrs.replace(m[0], `src=${m[1]}${url}${m[1]}`).replace(/\s+integrity\s*=\s*(["'])[^"']*\1/gi, "");
200
+ changes.push(`${LABEL[lib]}: <script> \u2192 ${url}${hadIntegrity ? " (old integrity hash removed)" : ""}`);
201
+ return `<script${next}>`;
202
+ });
203
+ }
204
+ function dropAosCssLinks(src, changes) {
205
+ let n = 0;
206
+ const out = src.replace(/^[ \t]*<link\b[^>]*>[ \t]*\r?\n?/gim, (line) => {
207
+ const m = /\bhref\s*=\s*(["'])([^"']+)\1/i.exec(line);
208
+ if (m && AOS_CSS_URL.test(m[2])) {
209
+ n++;
210
+ return "";
211
+ }
212
+ return line;
213
+ });
214
+ if (n) changes.push(`AOS: removed ${n} aos.css <link> (the drop-in ships its styles)`);
215
+ return out;
216
+ }
217
+ function dropAosCssAtImports(src, changes) {
218
+ let n = 0;
219
+ const out = src.replace(
220
+ /^[ \t]*@import\s+(?:url\(\s*)?(['"]?)~?aos\/dist\/aos(?:\.min)?\.css\1\s*\)?\s*;?[ \t]*\r?\n?/gm,
221
+ () => {
222
+ n++;
223
+ return "";
224
+ }
225
+ );
226
+ if (n) changes.push(`AOS: removed ${n} @import of aos.css (the drop-in ships its styles)`);
227
+ return out;
228
+ }
229
+ var SCRIPT_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i;
230
+ var HTML_EXT = /\.(?:html?|vue|svelte|astro|php|ejs|hbs|njk|liquid|erb)$/i;
231
+ var CSS_EXT = /\.(?:css|scss|sass|less)$/i;
232
+ function migrateSource(src, filename, version) {
233
+ const changes = [];
234
+ let code = src;
235
+ if (SCRIPT_EXT.test(filename)) {
236
+ code = dropAosCssImports(code, changes);
237
+ code = rewriteSpecifiers(code, changes);
238
+ }
239
+ if (HTML_EXT.test(filename)) {
240
+ code = rewriteScriptTags(code, version, changes);
241
+ code = dropAosCssLinks(code, changes);
242
+ }
243
+ if (CSS_EXT.test(filename) || /\.(?:vue|svelte|astro)$/i.test(filename)) {
244
+ code = dropAosCssAtImports(code, changes);
245
+ }
246
+ return { code, changes };
247
+ }
248
+ function migratePackageJson(text, version) {
249
+ const changes = [];
250
+ let pkg;
251
+ try {
252
+ pkg = JSON.parse(text);
253
+ } catch {
254
+ return { code: text, changes };
255
+ }
256
+ const remove = [...LIBS, "@types/aos"];
257
+ let home = null;
258
+ for (const field of ["dependencies", "devDependencies"]) {
259
+ const deps = pkg[field];
260
+ if (!deps) continue;
261
+ for (const name of remove) {
262
+ if (name in deps) {
263
+ delete deps[name];
264
+ changes.push(`package.json: removed ${name} from ${field}`);
265
+ if (!home && name !== "@types/aos") home = field;
266
+ }
267
+ }
268
+ }
269
+ if (!home) return { code: text, changes };
270
+ const already = ["dependencies", "devDependencies"].some(
271
+ (f) => pkg[f]?.["svg-scroll-draw"]
272
+ );
273
+ if (!already) {
274
+ const deps = pkg[home] ?? (pkg[home] = {});
275
+ deps["svg-scroll-draw"] = `^${version}`;
276
+ pkg[home] = Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
277
+ changes.push(`package.json: added svg-scroll-draw ^${version} to ${home}`);
278
+ }
279
+ const indent = /^\{\r?\n(\t| +)/.exec(text)?.[1] ?? " ";
280
+ return { code: JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : ""), changes };
281
+ }
282
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
283
+ "node_modules",
284
+ ".git",
285
+ "dist",
286
+ "build",
287
+ "out",
288
+ ".next",
289
+ ".nuxt",
290
+ ".output",
291
+ ".svelte-kit",
292
+ ".vercel",
293
+ ".netlify",
294
+ ".turbo",
295
+ ".cache",
296
+ "coverage",
297
+ ".parcel-cache",
298
+ "vendor"
299
+ ]);
300
+ var MAX_BYTES = 2 * 1024 * 1024;
301
+ function* walk(dir) {
302
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
303
+ if (entry.isDirectory()) {
304
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) yield* walk(path.join(dir, entry.name));
305
+ } else if (entry.isFile()) {
306
+ yield path.join(dir, entry.name);
307
+ }
308
+ }
309
+ }
310
+ function installCommand(dir) {
311
+ if (fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) return "pnpm install";
312
+ if (fs.existsSync(path.join(dir, "yarn.lock"))) return "yarn install";
313
+ if (fs.existsSync(path.join(dir, "bun.lockb")) || fs.existsSync(path.join(dir, "bun.lock"))) return "bun install";
314
+ return "npm install";
315
+ }
316
+ function gitDirty(dir) {
317
+ try {
318
+ return (0, import_node_child_process.execSync)("git status --porcelain", { cwd: dir, stdio: ["ignore", "pipe", "ignore"] }).toString().trim() !== "";
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
323
+ var HELP = `
324
+ svg-scroll-draw migrate [dir] [--dry-run] [--force]
325
+
326
+ Moves a project off AOS, ScrollReveal and vivus onto svg-scroll-draw's drop-ins:
327
+ \u2022 imports / requires of 'aos', 'scrollreveal', 'vivus' \u2192 svg-scroll-draw/<lib>
328
+ \u2022 CDN <script> tags \u2192 svg-scroll-draw's script-tag builds
329
+ \u2022 AOS's aos.css imports, <link>s and @imports removed (the drop-in ships its styles)
330
+ \u2022 package.json: the old packages swapped for svg-scroll-draw
331
+
332
+ --dry-run show what would change, write nothing
333
+ --force write even if the git working tree has uncommitted changes
334
+ `;
335
+ function runMigrate(args, version, log = console.log) {
336
+ if (args.includes("--help") || args.includes("-h")) {
337
+ log(HELP);
338
+ return 0;
339
+ }
340
+ const dryRun = args.includes("--dry-run");
341
+ const force = args.includes("--force");
342
+ const dir = path.resolve(args.find((a) => !a.startsWith("-")) ?? ".");
343
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
344
+ log(` \u2717 Not a directory: ${dir}`);
345
+ return 1;
346
+ }
347
+ const edits = [];
348
+ for (const file of walk(dir)) {
349
+ const base = path.basename(file);
350
+ const isPkg = base === "package.json";
351
+ if (!isPkg && !SCRIPT_EXT.test(base) && !HTML_EXT.test(base) && !CSS_EXT.test(base)) continue;
352
+ if (fs.statSync(file).size > MAX_BYTES) continue;
353
+ const text = fs.readFileSync(file, "utf8");
354
+ if (!isPkg && !/aos|scrollreveal|vivus/i.test(text)) continue;
355
+ const result = isPkg ? migratePackageJson(text, version) : migrateSource(text, base, version);
356
+ if (result.changes.length && result.code !== text) edits.push({ file, code: result.code, changes: result.changes });
357
+ }
358
+ log(`
359
+ svg-scroll-draw migrate${dryRun ? " (dry run)" : ""} \u2014 ${dir}
360
+ `);
361
+ if (!edits.length) {
362
+ log(" No AOS, ScrollReveal or vivus usage found. Nothing to do.\n");
363
+ return 0;
364
+ }
365
+ for (const { file, changes } of edits) {
366
+ log(` ${path.relative(dir, file) || path.basename(file)}`);
367
+ for (const c of changes) log(` \u2022 ${c}`);
368
+ }
369
+ if (dryRun) {
370
+ log(`
371
+ ${edits.length} file${edits.length === 1 ? "" : "s"} would change. Run again without --dry-run to apply.
372
+ `);
373
+ return 0;
374
+ }
375
+ if (!force && gitDirty(dir)) {
376
+ log("\n \u2717 The git working tree has uncommitted changes. Commit or stash them first so the");
377
+ log(" migration is a clean diff you can review, or pass --force to write anyway.\n");
378
+ return 1;
379
+ }
380
+ for (const { file, code } of edits) fs.writeFileSync(file, code);
381
+ const touchedPkg = edits.some((e) => path.basename(e.file) === "package.json");
382
+ log(`
383
+ \u2713 Updated ${edits.length} file${edits.length === 1 ? "" : "s"}.`);
384
+ log(` Next: ${touchedPkg ? `run \`${installCommand(dir)}\`, then ` : ""}review the diff (git diff) and try the page.
385
+ `);
386
+ return 0;
387
+ }
388
+
141
389
  // src/cli/init.ts
142
390
  var RESET = "\x1B[0m";
143
391
  var BOLD = "\x1B[1m";
@@ -147,19 +395,19 @@ var GREEN = "\x1B[32m";
147
395
  var CYAN = "\x1B[36m";
148
396
  function ask(rl, question, fallback) {
149
397
  return new Promise(
150
- (resolve) => rl.question(` ${CYAN}?${RESET} ${question} ${DIM}(${fallback})${RESET} `, (ans) => {
151
- resolve(ans.trim() || fallback);
398
+ (resolve2) => rl.question(` ${CYAN}?${RESET} ${question} ${DIM}(${fallback})${RESET} `, (ans) => {
399
+ resolve2(ans.trim() || fallback);
152
400
  })
153
401
  );
154
402
  }
155
403
  function pick(rl, question, options, fallback) {
156
404
  const opts = options.map((o, i) => `${DIM}${i + 1})${RESET} ${o}`).join(" ");
157
405
  return new Promise(
158
- (resolve) => rl.question(` ${CYAN}?${RESET} ${question}
406
+ (resolve2) => rl.question(` ${CYAN}?${RESET} ${question}
159
407
  ${opts}
160
408
  ${DIM}(${fallback})${RESET} `, (ans) => {
161
409
  const idx = parseInt(ans) - 1;
162
- resolve(idx >= 0 && idx < options.length ? options[idx] : fallback);
410
+ resolve2(idx >= 0 && idx < options.length ? options[idx] : fallback);
163
411
  })
164
412
  );
165
413
  }
@@ -179,8 +427,8 @@ async function main() {
179
427
  }
180
428
  const selector = needsSelector(framework) ? await ask(rl, "CSS selector for your SVG container?", "#my-svg") : "#my-svg";
181
429
  const outFile = FILE_NAMES[framework] ?? "scroll-draw.js";
182
- const outPath = path.join(process.cwd(), outFile);
183
- if (fs.existsSync(outPath)) {
430
+ const outPath = path2.join(process.cwd(), outFile);
431
+ if (fs2.existsSync(outPath)) {
184
432
  const overwrite = await ask(rl, `${outFile} already exists. Overwrite?`, "y");
185
433
  if (overwrite.toLowerCase() !== "y") {
186
434
  console.log(`
@@ -190,7 +438,7 @@ async function main() {
190
438
  }
191
439
  }
192
440
  const code = GENERATORS[framework]?.({ selector, easing, preset }) ?? "";
193
- fs.writeFileSync(outPath, code, "utf8");
441
+ fs2.writeFileSync(outPath, code, "utf8");
194
442
  console.log(`
195
443
  ${GREEN}\u2713${RESET} ${BOLD}${outFile}${RESET} created.
196
444
  `);
@@ -208,7 +456,30 @@ async function main() {
208
456
  rl.close();
209
457
  }
210
458
  }
211
- main().catch((e) => {
212
- console.error(e);
213
- process.exit(1);
214
- });
459
+ function ownVersion() {
460
+ try {
461
+ return JSON.parse(fs2.readFileSync(path2.join(__dirname, "..", "..", "package.json"), "utf8")).version;
462
+ } catch {
463
+ return "latest";
464
+ }
465
+ }
466
+ var USAGE = `
467
+ npx svg-scroll-draw init scaffold a starter file
468
+ npx svg-scroll-draw migrate switch a project from AOS, ScrollReveal or vivus (--dry-run to preview)
469
+ `;
470
+ var [command, ...rest] = process.argv.slice(2);
471
+ if (command === "help" || command === "--help" || command === "-h") {
472
+ console.log(USAGE);
473
+ } else if (command === "migrate") {
474
+ process.exitCode = runMigrate(rest, ownVersion());
475
+ } else if (command === void 0 || command === "init") {
476
+ main().catch((e) => {
477
+ console.error(e);
478
+ process.exit(1);
479
+ });
480
+ } else {
481
+ console.error(`
482
+ Unknown command "${command}".
483
+ ${USAGE}`);
484
+ process.exitCode = 1;
485
+ }
@@ -0,0 +1,12 @@
1
+ declare function enable(): void;
2
+ declare function disable(): void;
3
+ declare function toggle(): void;
4
+ declare function highlight(target: string | Element): void;
5
+ declare const devtools: {
6
+ enable: typeof enable;
7
+ disable: typeof disable;
8
+ toggle: typeof toggle;
9
+ highlight: typeof highlight;
10
+ };
11
+
12
+ export { devtools };
@@ -0,0 +1,12 @@
1
+ declare function enable(): void;
2
+ declare function disable(): void;
3
+ declare function toggle(): void;
4
+ declare function highlight(target: string | Element): void;
5
+ declare const devtools: {
6
+ enable: typeof enable;
7
+ disable: typeof disable;
8
+ toggle: typeof toggle;
9
+ highlight: typeof highlight;
10
+ };
11
+
12
+ export { devtools };
@@ -0,0 +1,52 @@
1
+ type EasingName = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring' | 'bounce' | 'elastic';
2
+ interface TriggerConfig {
3
+ start?: string;
4
+ end?: string;
5
+ }
6
+ interface ScrollDrawInstance {
7
+ destroy: () => void;
8
+ /** Reset and replay the animation from the beginning. */
9
+ replay: () => void;
10
+ /** Pause the animation at the current progress. */
11
+ pause: () => void;
12
+ /** Resume a paused animation. */
13
+ resume: () => void;
14
+ /** Jump to a specific progress value (0–1) and pause. */
15
+ seek: (progress: number) => void;
16
+ /** Returns current draw progress (0–1). */
17
+ getProgress: () => number;
18
+ /**
19
+ * Re-measure after a layout change: path lengths, and the scroll trigger window.
20
+ *
21
+ * The engine already re-measures on `resize`, on `orientationchange`, and via a
22
+ * `ResizeObserver` on the document element — but none of those fire for a layout
23
+ * change that leaves the document height alone, such as a tab switching, a
24
+ * sibling collapsing, or a font swapping inside a fixed-height box. Call this
25
+ * after one of those.
26
+ *
27
+ * Optional on this shared type because several instances here are fan-outs over
28
+ * other instances or SSR no-op stubs. `scrollDraw`, `scrollPin`,
29
+ * `scrollHorizontal`, `scrollDrawTimeline` and the group APIs all provide it;
30
+ * use `instance.refresh?.()` if you hold an instance of unknown origin.
31
+ */
32
+ refresh?: () => void;
33
+ }
34
+
35
+ interface ScrollTextOptions {
36
+ split?: 'chars' | 'words' | 'lines';
37
+ stagger?: number;
38
+ easing?: EasingName | ((t: number) => number);
39
+ from?: {
40
+ opacity?: number;
41
+ y?: number;
42
+ x?: number;
43
+ rotate?: number;
44
+ scale?: number;
45
+ };
46
+ trigger?: TriggerConfig;
47
+ once?: boolean;
48
+ onComplete?: () => void;
49
+ }
50
+ declare function scrollText(target: string | Element, options?: ScrollTextOptions): ScrollDrawInstance;
51
+
52
+ export { type ScrollTextOptions, scrollText };
@@ -0,0 +1,52 @@
1
+ type EasingName = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring' | 'bounce' | 'elastic';
2
+ interface TriggerConfig {
3
+ start?: string;
4
+ end?: string;
5
+ }
6
+ interface ScrollDrawInstance {
7
+ destroy: () => void;
8
+ /** Reset and replay the animation from the beginning. */
9
+ replay: () => void;
10
+ /** Pause the animation at the current progress. */
11
+ pause: () => void;
12
+ /** Resume a paused animation. */
13
+ resume: () => void;
14
+ /** Jump to a specific progress value (0–1) and pause. */
15
+ seek: (progress: number) => void;
16
+ /** Returns current draw progress (0–1). */
17
+ getProgress: () => number;
18
+ /**
19
+ * Re-measure after a layout change: path lengths, and the scroll trigger window.
20
+ *
21
+ * The engine already re-measures on `resize`, on `orientationchange`, and via a
22
+ * `ResizeObserver` on the document element — but none of those fire for a layout
23
+ * change that leaves the document height alone, such as a tab switching, a
24
+ * sibling collapsing, or a font swapping inside a fixed-height box. Call this
25
+ * after one of those.
26
+ *
27
+ * Optional on this shared type because several instances here are fan-outs over
28
+ * other instances or SSR no-op stubs. `scrollDraw`, `scrollPin`,
29
+ * `scrollHorizontal`, `scrollDrawTimeline` and the group APIs all provide it;
30
+ * use `instance.refresh?.()` if you hold an instance of unknown origin.
31
+ */
32
+ refresh?: () => void;
33
+ }
34
+
35
+ interface ScrollTextOptions {
36
+ split?: 'chars' | 'words' | 'lines';
37
+ stagger?: number;
38
+ easing?: EasingName | ((t: number) => number);
39
+ from?: {
40
+ opacity?: number;
41
+ y?: number;
42
+ x?: number;
43
+ rotate?: number;
44
+ scale?: number;
45
+ };
46
+ trigger?: TriggerConfig;
47
+ once?: boolean;
48
+ onComplete?: () => void;
49
+ }
50
+ declare function scrollText(target: string | Element, options?: ScrollTextOptions): ScrollDrawInstance;
51
+
52
+ export { type ScrollTextOptions, scrollText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svg-scroll-draw",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Scroll-driven animation for the web — draw SVG paths, reveal elements, pin sections, scrub video. Zero dependencies, native CSS fast path, works with React, Vue, Svelte, Solid, Angular, Astro and vanilla JS",
5
5
  "keywords": [
6
6
  "svg",