@cosmictraveler002/anim-kit 1.0.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1033 -0
  3. package/dist/anim-kit.standalone.js +117 -0
  4. package/dist/anim-kit.standalone.js.map +1 -0
  5. package/dist/core/gsap.d.ts +44 -0
  6. package/dist/core/gsap.d.ts.map +1 -0
  7. package/dist/core/gsap.js +71 -0
  8. package/dist/core/gsap.js.map +1 -0
  9. package/dist/core/guard.d.ts +3 -0
  10. package/dist/core/guard.d.ts.map +1 -0
  11. package/dist/core/guard.js +18 -0
  12. package/dist/core/guard.js.map +1 -0
  13. package/dist/core/smooth-scroll.d.ts +45 -0
  14. package/dist/core/smooth-scroll.d.ts.map +1 -0
  15. package/dist/core/smooth-scroll.js +98 -0
  16. package/dist/core/smooth-scroll.js.map +1 -0
  17. package/dist/core/split.d.ts +24 -0
  18. package/dist/core/split.d.ts.map +1 -0
  19. package/dist/core/split.js +117 -0
  20. package/dist/core/split.js.map +1 -0
  21. package/dist/core/types.d.ts +21 -0
  22. package/dist/core/types.d.ts.map +1 -0
  23. package/dist/core/types.js +9 -0
  24. package/dist/core/types.js.map +1 -0
  25. package/dist/core/util.d.ts +19 -0
  26. package/dist/core/util.d.ts.map +1 -0
  27. package/dist/core/util.js +73 -0
  28. package/dist/core/util.js.map +1 -0
  29. package/dist/effects/counter.d.ts +52 -0
  30. package/dist/effects/counter.d.ts.map +1 -0
  31. package/dist/effects/counter.js +106 -0
  32. package/dist/effects/counter.js.map +1 -0
  33. package/dist/effects/cursor-follower.d.ts +19 -0
  34. package/dist/effects/cursor-follower.d.ts.map +1 -0
  35. package/dist/effects/cursor-follower.js +76 -0
  36. package/dist/effects/cursor-follower.js.map +1 -0
  37. package/dist/effects/drag-strip.d.ts +17 -0
  38. package/dist/effects/drag-strip.d.ts.map +1 -0
  39. package/dist/effects/drag-strip.js +186 -0
  40. package/dist/effects/drag-strip.js.map +1 -0
  41. package/dist/effects/hero-shrink.d.ts +15 -0
  42. package/dist/effects/hero-shrink.d.ts.map +1 -0
  43. package/dist/effects/hero-shrink.js +40 -0
  44. package/dist/effects/hero-shrink.js.map +1 -0
  45. package/dist/effects/horizontal-scroll.d.ts +17 -0
  46. package/dist/effects/horizontal-scroll.d.ts.map +1 -0
  47. package/dist/effects/horizontal-scroll.js +87 -0
  48. package/dist/effects/horizontal-scroll.js.map +1 -0
  49. package/dist/effects/line-reveal.d.ts +34 -0
  50. package/dist/effects/line-reveal.d.ts.map +1 -0
  51. package/dist/effects/line-reveal.js +71 -0
  52. package/dist/effects/line-reveal.js.map +1 -0
  53. package/dist/effects/liquid-button.d.ts +19 -0
  54. package/dist/effects/liquid-button.d.ts.map +1 -0
  55. package/dist/effects/liquid-button.js +52 -0
  56. package/dist/effects/liquid-button.js.map +1 -0
  57. package/dist/effects/logo-reveal.d.ts +15 -0
  58. package/dist/effects/logo-reveal.d.ts.map +1 -0
  59. package/dist/effects/logo-reveal.js +49 -0
  60. package/dist/effects/logo-reveal.js.map +1 -0
  61. package/dist/effects/marquee.d.ts +13 -0
  62. package/dist/effects/marquee.d.ts.map +1 -0
  63. package/dist/effects/marquee.js +143 -0
  64. package/dist/effects/marquee.js.map +1 -0
  65. package/dist/effects/mask-reveal.d.ts +36 -0
  66. package/dist/effects/mask-reveal.d.ts.map +1 -0
  67. package/dist/effects/mask-reveal.js +102 -0
  68. package/dist/effects/mask-reveal.js.map +1 -0
  69. package/dist/effects/menu-overlay.d.ts +32 -0
  70. package/dist/effects/menu-overlay.d.ts.map +1 -0
  71. package/dist/effects/menu-overlay.js +150 -0
  72. package/dist/effects/menu-overlay.js.map +1 -0
  73. package/dist/effects/nav-hide.d.ts +13 -0
  74. package/dist/effects/nav-hide.d.ts.map +1 -0
  75. package/dist/effects/nav-hide.js +68 -0
  76. package/dist/effects/nav-hide.js.map +1 -0
  77. package/dist/effects/parallax.d.ts +11 -0
  78. package/dist/effects/parallax.d.ts.map +1 -0
  79. package/dist/effects/parallax.js +48 -0
  80. package/dist/effects/parallax.js.map +1 -0
  81. package/dist/effects/preloader.d.ts +33 -0
  82. package/dist/effects/preloader.d.ts.map +1 -0
  83. package/dist/effects/preloader.js +128 -0
  84. package/dist/effects/preloader.js.map +1 -0
  85. package/dist/effects/scatter-text.d.ts +20 -0
  86. package/dist/effects/scatter-text.d.ts.map +1 -0
  87. package/dist/effects/scatter-text.js +87 -0
  88. package/dist/effects/scatter-text.js.map +1 -0
  89. package/dist/effects/stacked-cards.d.ts +24 -0
  90. package/dist/effects/stacked-cards.d.ts.map +1 -0
  91. package/dist/effects/stacked-cards.js +106 -0
  92. package/dist/effects/stacked-cards.js.map +1 -0
  93. package/dist/effects/theme-reveal.d.ts +35 -0
  94. package/dist/effects/theme-reveal.d.ts.map +1 -0
  95. package/dist/effects/theme-reveal.js +93 -0
  96. package/dist/effects/theme-reveal.js.map +1 -0
  97. package/dist/index.d.ts +77 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +61 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/styles/anim-kit.css +361 -0
  102. package/package.json +80 -0
  103. package/src/core/gsap.ts +79 -0
  104. package/src/core/guard.ts +18 -0
  105. package/src/core/smooth-scroll.ts +143 -0
  106. package/src/core/split.ts +145 -0
  107. package/src/core/types.ts +30 -0
  108. package/src/core/util.ts +79 -0
  109. package/src/effects/counter.ts +192 -0
  110. package/src/effects/cursor-follower.ts +104 -0
  111. package/src/effects/drag-strip.ts +228 -0
  112. package/src/effects/hero-shrink.ts +69 -0
  113. package/src/effects/horizontal-scroll.ts +123 -0
  114. package/src/effects/line-reveal.ts +109 -0
  115. package/src/effects/liquid-button.ts +75 -0
  116. package/src/effects/logo-reveal.ts +76 -0
  117. package/src/effects/marquee.ts +157 -0
  118. package/src/effects/mask-reveal.ts +148 -0
  119. package/src/effects/menu-overlay.ts +218 -0
  120. package/src/effects/nav-hide.ts +90 -0
  121. package/src/effects/parallax.ts +68 -0
  122. package/src/effects/preloader.ts +187 -0
  123. package/src/effects/scatter-text.ts +129 -0
  124. package/src/effects/stacked-cards.ts +154 -0
  125. package/src/effects/theme-reveal.ts +144 -0
  126. package/src/index.ts +98 -0
  127. package/src/styles/anim-kit.css +361 -0
package/README.md ADDED
@@ -0,0 +1,1033 @@
1
+ # anim-kit
2
+
3
+ A modular, framework-agnostic animation library extracted from
4
+ [dzinrstudio.com](https://dzinrstudio.com/) — GSAP + ScrollTrigger + Lenis
5
+ effects packaged as independent ES modules.
6
+
7
+ TypeScript source → compiled ESM + `.d.ts` output. No framework, no virtual DOM,
8
+ no components: every effect resolves plain DOM selectors, so it works with
9
+ **React, Vue, Next, Svelte, Astro or plain HTML**.
10
+
11
+ ```ts
12
+ import { lineReveal, marquee, menuOverlay } from "anim-kit";
13
+
14
+ const destroy = lineReveal("[data-lines]", { mode: "scroll" });
15
+ // …later (route change, HMR, teardown):
16
+ destroy();
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Contents
22
+
23
+ - [Install](#install)
24
+ - [CDN usage](#cdn-usage)
25
+ - [Quick start](#quick-start)
26
+ - [The contract](#the-contract)
27
+ - [Smooth scroll](#smooth-scroll)
28
+ - [Effect categories](#effect-categories)
29
+ - [API](#api)
30
+ - [Core](#core)
31
+ - [Text animations](#text-animations)
32
+ - [Scroll & media](#scroll--media)
33
+ - [Loops & marquees](#loops--marquees)
34
+ - [Buttons & links](#buttons--links)
35
+ - [Navigation & overlays](#navigation--overlays)
36
+ - [Intros & transitions](#intros--transitions)
37
+ - [Logos & SVG](#logos--svg)
38
+ - [Utilities](#utilities)
39
+ - [Styling](#styling)
40
+ - [Reduced motion](#reduced-motion)
41
+ - [Framework integration](#framework-integration)
42
+ - [Demo & tests](#demo--tests)
43
+ - [Copy-prompt API](#copy-prompt-api)
44
+ - [Project structure](#project-structure)
45
+
46
+ ---
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ npm install anim-kit
52
+ ```
53
+
54
+ ```ts
55
+ import { smoothScroll, horizontalScroll } from "anim-kit";
56
+ import "anim-kit/styles"; // companion stylesheet (plain .css, optional but recommended)
57
+ ```
58
+
59
+ `anim-kit` ships **ESM only** with generated `.d.ts` declarations — no CJS
60
+ build, no runtime CSS-in-JS. `gsap` and `lenis` are regular `dependencies`,
61
+ so any bundler (Vite / Next / webpack / Remix) resolves them for you.
62
+
63
+ ### From source (development)
64
+
65
+ ```bash
66
+ git clone <this repo>
67
+ cd anim-kit
68
+ npm install
69
+ npm run build # tsc → dist/ (ESM + .d.ts) + CSS copy + tsup standalone bundle
70
+ npm test # build + unit smoke + demo wiring smoke
71
+ npm run demo # visual demo on http://localhost:4321/demo/
72
+ ```
73
+
74
+ ### Subpath exports
75
+
76
+ Everything the `exports` map in `package.json` exposes (all paths resolve
77
+ inside the published tarball):
78
+
79
+ | Specifier | Resolves to | Use for |
80
+ |---|---|---|
81
+ | `anim-kit` | `dist/index.js` + `dist/index.d.ts` | the full barrel — 38 exports |
82
+ | `anim-kit/effects/<name>` | `dist/effects/<name>.js` + `.d.ts` | one effect in isolation (`marquee`, `lineReveal`, …) |
83
+ | `anim-kit/standalone` | `dist/anim-kit.standalone.js` (types → `index.d.ts`) | the self-contained bundle — same API |
84
+ | `anim-kit/styles` | `dist/styles/anim-kit.css` | untouched plain CSS |
85
+
86
+ ```ts
87
+ import { marquee } from "anim-kit/effects/marquee"; // deep import, no barrel
88
+ import "anim-kit/styles";
89
+ ```
90
+
91
+ TypeScript ≥ 4.7 with `moduleResolution: "bundler"` or `"node16"`/`"nodenext"`
92
+ resolves declarations through the same map — no `typesVersions` shim needed.
93
+
94
+ ---
95
+
96
+ ## CDN usage
97
+
98
+ No build step on the consumer's end: `dist/` is served as-is from the npm
99
+ tarball by any npm CDN. Every URL is **version-pinned** — npm versions are
100
+ immutable, so `anim-kit@1.0.0` always resolves to exactly that build, forever
101
+ (only a new version creates a new URL; nothing floats unless you ask for a
102
+ range).
103
+
104
+ ### Option 1 — standalone bundle (simplest)
105
+
106
+ `dist/anim-kit.standalone.js` is a self-contained ESM bundle with `gsap` (+ the
107
+ plugins anim-kit uses) and `lenis` **inlined** — no import map, one URL, works
108
+ identically on jsDelivr and unpkg:
109
+
110
+ ```html
111
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/anim-kit@1.0.0/dist/styles/anim-kit.css" />
112
+
113
+ <script type="module">
114
+ import {
115
+ smoothScroll, lineReveal, marquee,
116
+ } from "https://cdn.jsdelivr.net/npm/anim-kit@1.0.0/dist/anim-kit.standalone.js";
117
+
118
+ smoothScroll();
119
+ lineReveal("[data-lines]", { mode: "scroll" });
120
+ marquee("[data-marquee-track]", { speed: 40 });
121
+ </script>
122
+ ```
123
+
124
+ unpkg serves the same file: `https://unpkg.com/anim-kit@1.0.0/dist/anim-kit.standalone.js`
125
+
126
+ ### Option 2 — jsDelivr `+esm`
127
+
128
+ jsDelivr bundles `anim-kit` with its dependencies on the fly (also immutable
129
+ per version):
130
+
131
+ ```html
132
+ <script type="module">
133
+ import { lineReveal } from "https://cdn.jsdelivr.net/npm/anim-kit@1.0.0/+esm";
134
+ </script>
135
+ ```
136
+
137
+ ### Option 3 — per-file ESM + import map (jsDelivr *and* unpkg)
138
+
139
+ The unbundled `dist/index.js` contains bare imports (`gsap`, `lenis`), so pin
140
+ them in an import map. This is the exact shape the [demo](#demo--tests) runs
141
+ locally, with CDN URLs — and the way to share one GSAP between anim-kit and
142
+ the rest of your page:
143
+
144
+ ```html
145
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/anim-kit@1.0.0/dist/styles/anim-kit.css" />
146
+
147
+ <script type="importmap">
148
+ {
149
+ "imports": {
150
+ "anim-kit": "https://cdn.jsdelivr.net/npm/anim-kit@1.0.0/dist/index.js",
151
+ "gsap": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/index.js",
152
+ "gsap/ScrollTrigger": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/ScrollTrigger.js",
153
+ "gsap/SplitText": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/SplitText.js",
154
+ "gsap/Draggable": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/Draggable.js",
155
+ "gsap/CustomEase": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/CustomEase.js",
156
+ "gsap/ScrollSmoother": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/ScrollSmoother.js",
157
+ "lenis": "https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.mjs"
158
+ }
159
+ }
160
+ </script>
161
+
162
+ <script type="module">
163
+ import { smoothScroll, lineReveal } from "anim-kit";
164
+ // per-effect deep imports work here too:
165
+ // import { dragStrip } from "anim-kit/effects/dragStrip";
166
+ </script>
167
+ ```
168
+
169
+ Swap the host for unpkg (`https://unpkg.com/anim-kit@1.0.0/dist/index.js`, …) —
170
+ the file layout is identical. GSAP subpaths are listed one by one because
171
+ import maps match specifiers literally: a trailing-slash prefix map would
172
+ produce extension-less URLs, which CDNs don't serve. The `gsap`/`lenis` pins
173
+ match `package-lock.json`.
174
+
175
+ ---
176
+
177
+ ## Quick start
178
+
179
+ ```html
180
+ <p data-lines>Every line of this paragraph is masked and slid up on scroll.</p>
181
+ <div class="ak-marquee">
182
+ <div class="ak-marquee__viewport">
183
+ <div class="ak-marquee__track" data-marquee-track data-dir="left">
184
+ <span>Prink</span><span>Zerodha</span><span>Superyou</span>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ ```
189
+
190
+ ```js
191
+ import { smoothScroll, lineReveal, marquee, compose } from "anim-kit";
192
+
193
+ const scroller = smoothScroll({ lerp: 0.08, smoothWheel: true });
194
+
195
+ const teardown = compose(
196
+ scroller.destroy,
197
+ lineReveal("[data-lines]", { mode: "scroll" }),
198
+ marquee("[data-marquee-track]", { speed: 40 }),
199
+ );
200
+
201
+ window.addEventListener("pagehide", () => teardown(), { once: true });
202
+ ```
203
+
204
+ ---
205
+
206
+ ## The contract
207
+
208
+ Every effect follows one shape:
209
+
210
+ ```ts
211
+ effect(target, options) => destroy
212
+ ```
213
+
214
+ - **`target`** — anything assignable to `TargetLike`: a selector `string`, an
215
+ `Element`, an array of elements, a `NodeList`, or `null`/`undefined`.
216
+ - **`options`** — a plain object of documented, defaulted fields. Every options
217
+ object also accepts `force?: boolean` (see [Reduced motion](#reduced-motion)).
218
+ - **`destroy`** — a `() => void` that kills tweens/ScrollTriggers (paused
219
+ springs included), removes listeners and clones, restores the original
220
+ markup, and reverts the inline styles the effect overwrote. Always safe to
221
+ call once; call it on teardown.
222
+
223
+ A handful of effects need more than a destroy function and return a **handle**
224
+ instead — those handles still expose `.destroy()`:
225
+
226
+ | Effect | Handle |
227
+ | --------------- | -------------------------------------------- |
228
+ | `smoothScroll` | `{ lenis, scrollTo, active, destroy }` |
229
+ | `menuOverlay` | `{ open, close, toggle, isOpen, destroy }` |
230
+ | `themeReveal` | `{ set, toggle, current, destroy }` |
231
+ | `audioBars` | `{ start, stop, destroy }` |
232
+
233
+ **Missing targets never throw.** If nothing matches, the effect returns an
234
+ immediate no-op destroy — safe to call during progressive enhancement.
235
+
236
+ `initGSAP()` runs automatically inside every effect (registration is
237
+ idempotent), but you can call it yourself if you want `gsap`/`ScrollTrigger`
238
+ configured before first paint.
239
+
240
+ ---
241
+
242
+ ## Smooth scroll
243
+
244
+ Lenis is wired to ScrollTrigger the canonical way: Lenis drives the scroll,
245
+ `lenis.on("scroll", ScrollTrigger.update)` keeps triggers in sync, `lenis.raf`
246
+ is ticked from `gsap.ticker`, and `lagSmoothing(0)` is disabled.
247
+
248
+ ```ts
249
+ const scroller = smoothScroll({
250
+ lerp: 0.08, // interpolation factor (default) — lower = floatier
251
+ smoothWheel: true, // default
252
+ smoothTouch: false, // default; true fights native touch scrolling
253
+ orientation: "vertical",
254
+ initialScroll: 0,
255
+ useScrollerProxy: false, // opt-in scrollerProxy for nested scrollers
256
+ });
257
+
258
+ scroller.scrollTo("#work", { duration: 1.2 }); // programmatic, smoothed
259
+ scroller.active; // false when Lenis was skipped (e.g. reduced motion)
260
+ scroller.destroy();
261
+ ```
262
+
263
+ Call it **before** creating scroll effects so the first refresh sees the right
264
+ scroller. When motion is reduced, `smoothScroll` stays inert and native
265
+ scrolling is left alone.
266
+
267
+ ---
268
+
269
+ ## Effect categories
270
+
271
+ Every effect belongs to exactly one **category → subcategory** slot. The tree
272
+ below is the library's map: it orders this API reference, groups the demo's
273
+ prompt dock, and backs the `category` / `subcategory` fields on
274
+ `GET /api/prompts`.
275
+
276
+ | Category | Subcategory | Effects |
277
+ | --- | --- | --- |
278
+ | Core & setup | Smooth scrolling | `smoothScroll` |
279
+ | Text animations | Line & mask reveals | `lineReveal`, `maskReveal` |
280
+ | Text animations | Per-character scatter | `scatterText` |
281
+ | Text animations | Counters | `counter` |
282
+ | Scroll & media | Pinned galleries | `horizontalScroll`, `stackedCards`, `stackedCardsPinned` |
283
+ | Scroll & media | Parallax & depth | `parallax` |
284
+ | Scroll & media | Heroes & media | `heroShrink` |
285
+ | Scroll & media | Enter reveals | `revealRule` |
286
+ | Loops & marquees | Marquees | `marquee` |
287
+ | Loops & marquees | Infinite draggables | `dragStrip` |
288
+ | Loops & marquees | Equalizers | `audioBars` |
289
+ | Buttons & links | Liquid fills | `liquidButton` |
290
+ | Buttons & links | Underlines | `underlineLink` |
291
+ | Navigation & overlays | Menus & nav | `navHide`, `menuOverlay` |
292
+ | Navigation & overlays | Cursors | `cursorFollower` |
293
+ | Intros & transitions | Preloaders | `preloader` |
294
+ | Intros & transitions | Theme wipes | `themeReveal` |
295
+ | Logos & SVG | Path reveals | `logoReveal` |
296
+
297
+ **Growing the library:** a subcategory is the slot sibling effects land in —
298
+ add the id to `TAXONOMY` in `scripts/prompts.mjs`, export the effect from
299
+ `src/index.ts`, and give it a prompt entry. The demo smoke fails when an
300
+ effect is unclassified, classified twice, or the tree references an effect
301
+ that does not exist.
302
+
303
+ ---
304
+ ## API
305
+
306
+ ### Core
307
+
308
+ #### `initGSAP()`
309
+
310
+ Registers the plugins anim-kit relies on (`ScrollTrigger`, `SplitText`,
311
+ `Draggable`, `CustomEase`, `ScrollSmoother`) and the studio's custom eases.
312
+ Idempotent; called for you by every effect.
313
+
314
+ #### `EASES`
315
+
316
+ ```ts
317
+ EASES.curtain // "ak-curtain" .76,0,.24,1 — menu curtain, panel wipes
318
+ EASES.cardStack // "ak-card-stack" SVG cubic bezier — cascading card deck
319
+ EASES.reveal // "ak-reveal" .165,.84,.44,1 — mask/rule reveals
320
+ EASES.preloadOut // "ak-preload-out" .895,.03,.685,.22 — preloader exit
321
+ ```
322
+
323
+ Use them anywhere GSAP accepts an ease: `gsap.to(el, { ease: EASES.curtain })`.
324
+
325
+ #### `split(target, options) => { elements, revert }`
326
+
327
+ Text splitting with a dependency-free fallback if `SplitText` is unavailable.
328
+
329
+ ```ts
330
+ const { elements, revert } = split("h1", {
331
+ type: "lines", // "chars" | "words" | "lines" (or an array)
332
+ mask: true, // wrap each line in an overflow-hidden mask
333
+ linesClass: "ak-line++",// "++" is replaced by the index
334
+ lineThreshold: 0.05, // ignore lines shorter than 5% of the container
335
+ });
336
+ revert(); // restores the original markup exactly
337
+ ```
338
+
339
+ #### `guard(options, run) => destroy`
340
+
341
+ Central reduced-motion gate. If the user prefers reduced motion and
342
+ `options.force` is not set, it returns a no-op destroy; otherwise it runs
343
+ `run()`. Every effect goes through it.
344
+
345
+ ---
346
+
347
+ ### Text animations
348
+
349
+ Typography in motion — masked lines, rising masks, per-character scatter, tickers.
350
+
351
+ #### `lineReveal(target, options?) => destroy`
352
+
353
+ Splits text into masked lines and staggers them up. The signature reveal of the
354
+ source site.
355
+
356
+ ```ts
357
+ lineReveal("[data-hero-text]", { mode: "immediate", delay: 0.35 }); // above the fold
358
+ lineReveal("[data-lines]", { mode: "scroll" }); // reverses on leave
359
+ ```
360
+
361
+ | Option | Default | Notes |
362
+ | --------- | --------------- | --------------------------------------- |
363
+ | `mode` | `"scroll"` | `"scroll"` or `"immediate"` |
364
+ | `stagger` | `0.1` | seconds between lines |
365
+ | `duration`| `1` | seconds |
366
+ | `ease` | `"power4.out"` | |
367
+ | `delay` | `0` | seconds |
368
+ | `start` | `"top 90%"` | ScrollTrigger start |
369
+ | `end` | `"bottom 10%"` | ScrollTrigger end |
370
+
371
+ **DOM:** any block of text — headings with `<br>` hard breaks work. Produces
372
+ `.ak-line-mask > .ak-line` per line; `destroy()` restores the original HTML.
373
+
374
+ #### `maskReveal(target, options?) => destroy`
375
+
376
+ Inline `overflow:hidden` heading reveal: the inner span rises from below the
377
+ mask and settles.
378
+
379
+ ```html
380
+ <h2>
381
+ <span class="ak-mask"><span class="ak-mask__inner" data-mask>Text that</span></span>
382
+ <span class="ak-mask"><span class="ak-mask__inner" data-mask>rises into view.</span></span>
383
+ </h2>
384
+ ```
385
+
386
+ ```ts
387
+ maskReveal("[data-mask]"); // siblings inside one parent stagger together
388
+ ```
389
+
390
+ | Option | Default |
391
+ | --------- | ---------------- |
392
+ | `from` / `to` | `"100%"` / `"0%"` |
393
+ | `duration`| `0.5` |
394
+ | `ease` | `EASES.reveal` |
395
+ | `stagger` | `0.1` |
396
+ | `delay` | `0` |
397
+ | `start` / `end` | `"top 90%"` / `"bottom 10%"` |
398
+ | `mode` | `"scroll"` — or `"immediate"` to play at once |
399
+
400
+ #### `scatterText(wrap, options?) => destroy`
401
+
402
+ The giant pinned band ("So, are you ready to Stand out?"): the line scrolls
403
+ horizontally while each character starts at a random `yPercent`/rotation and
404
+ settles as it crosses the viewport.
405
+
406
+ ```ts
407
+ scatterText("[data-scatter-pin]", {
408
+ line: "[data-scatter]",
409
+ pinTarget: "[data-scatter-pin]",
410
+ granularity: "chars", // or "words"
411
+ scatterY: 60, // ±60% of line height
412
+ scatterRotation: 15, // ±15°
413
+ scrub: 0.5,
414
+ settleStart: "left 100%",
415
+ settleEnd: "left 15%",
416
+ });
417
+ ```
418
+
419
+ **DOM:** `.ak-char` / `.ak-space` spans are generated for you (and removed on
420
+ `destroy()`). Line measurement waits for `document.fonts.ready` so travel
421
+ distance is correct with webfonts.
422
+
423
+ #### `counter(target, options?) => destroy`
424
+
425
+ Tabular number ticker.
426
+
427
+ ```ts
428
+ counter("[data-count]", { to: 240, duration: 3, suffix: "+" });
429
+ counter("[data-count-scroll]", { to: 98, onScroll: true }); // waits for view
430
+ counter("[data-count-pad]", { to: 42, pad: 3 }); // 000 → 042
431
+ ```
432
+
433
+ | Option | Default |
434
+ | ------------ | -------------- |
435
+ | `from` / `to`| `0` / `100` |
436
+ | `duration` | `4` |
437
+ | `ease` | `"power1.inOut"` |
438
+ | `pad` | `0` (none) |
439
+ | `suffix` | `""` |
440
+ | `onScroll` | `false` |
441
+ | `start` | ScrollTrigger start when `onScroll` |
442
+ | `onComplete` | `(value) => {}` |
443
+
444
+ ---
445
+
446
+ ### Scroll & media
447
+
448
+ Scroll-driven storytelling — enter reveals, depth, pinned galleries, hero media.
449
+
450
+ #### `revealRule(target, options?) => destroy`
451
+
452
+ The thin rule that draws itself to full width.
453
+
454
+ ```ts
455
+ revealRule("[data-rule]", { duration: 1, delay: 0.2 }); // default ease: EASES.reveal
456
+ ```
457
+
458
+ **DOM:** any element that should animate `width: 0 → 100%` when it enters.
459
+
460
+ #### `parallax(target, options?) => destroy`
461
+
462
+ `data-speed` parallax over everything inside `target`.
463
+
464
+ ```html
465
+ <img data-speed="-0.5" src="…" /> <!-- slower than scroll -->
466
+ <img data-speed="0.8" src="…" /> <!-- faster than scroll -->
467
+ ```
468
+
469
+ ```ts
470
+ parallax("[data-parallax]", {
471
+ attribute: "data-speed",
472
+ scale: 50, // yPercent multiplier
473
+ start: "50% bottom",
474
+ end: "bottom top",
475
+ });
476
+ ```
477
+
478
+ Each element tweens `yPercent: value × scale` with `scrub: true` and
479
+ `ease: "none"` — so `data-speed="0.8"` settles at `yPercent: 40`; negative
480
+ speeds drift up against the scroll.
481
+
482
+ #### `horizontalScroll(track, options?) => destroy`
483
+
484
+ Pinned horizontal gallery; panel images get a secondary parallax driven by
485
+ `containerAnimation`, so they settle as they cross the viewport *horizontally*.
486
+
487
+ ```ts
488
+ horizontalScroll("[data-htrack]", {
489
+ section: "[data-hsection]", // pinned trigger (defaults to track's <section>)
490
+ panelImage: "[data-speed-img]", // extra parallax selector, null to disable
491
+ scrub: 0.5,
492
+ imageScrub: 0.2,
493
+ travel: () => 1500, // override the default scrollWidth − innerWidth
494
+ });
495
+ ```
496
+
497
+ **DOM:**
498
+
499
+ ```html
500
+ <section data-hsection>
501
+ <div class="h-track" data-htrack> <!-- width: max-content -->
502
+ <div class="h-panel">…<img data-speed-img></div> × N
503
+ </div>
504
+ </section>
505
+ ```
506
+
507
+ #### `stackedCards(wrap, options?) => destroy`
508
+
509
+ Pinned card deck — cards cascade with the `ak-card-stack` bezier.
510
+
511
+ ```ts
512
+ stackedCards("[data-stack-wrap]", {
513
+ viewport: "[data-stack-viewport]", // defaults to wrap's first child
514
+ card: ".ak-card",
515
+ scrub: 0.5,
516
+ stagger: 0.12,
517
+ ease: EASES.cardStack,
518
+ });
519
+ ```
520
+
521
+ **DOM:** a tall wrapper (e.g. `height: 500vh`) containing a sticky viewport that
522
+ holds the cards:
523
+
524
+ ```html
525
+ <div class="stack-wrap" data-stack-wrap> <!-- tall scroll runway -->
526
+ <div class="stack-viewport" data-stack-viewport> <!-- position: sticky; top: 0 -->
527
+ <div class="stack-cards">
528
+ <a class="ak-card">01 …</a> × N
529
+ </div>
530
+ </div>
531
+ </div>
532
+ ```
533
+
534
+ #### `stackedCardsPinned(wrap, { viewport, … }) => destroy`
535
+
536
+ Same deck for when the sticky viewport is a **sibling** rather than a child —
537
+ pins `viewport` with `pinSpacing: false` across `wrap`'s scroll length.
538
+ `viewport` is required here.
539
+
540
+ #### `heroShrink(target, options?) => destroy`
541
+
542
+ Hero media that scales down and drifts as it scrolls away.
543
+
544
+ ```ts
545
+ heroShrink("[data-hero-media]", { offsetY: "49vh", scale: 0.23, scrub: 1 });
546
+ // options: offsetX "0px", start "top top", end "bottom top"
547
+ ```
548
+
549
+ ---
550
+
551
+ ### Loops & marquees
552
+
553
+ Continuous motion — marquees, infinite draggables, equaliser bars.
554
+
555
+ #### `marquee(track, options?) => destroy`
556
+
557
+ Dual-row constant-speed marquee driven by `requestAnimationFrame` — 40 px/s,
558
+ matching the source site.
559
+
560
+ ```ts
561
+ marquee("[data-marquee-track]", { speed: 40, direction: "left", pauseOnHover: false });
562
+ ```
563
+
564
+ | Option | Default | Notes |
565
+ | -------------- | ------- | ------------------------------------------------- |
566
+ | `speed` | `40` | pixels per second |
567
+ | `direction` | `data-dir` | `"left"` / `"right"`; otherwise read from `data-dir` |
568
+ | `clone` | `true` | duplicate content when it isn't already doubled |
569
+ | `pauseOnHover` | `false` | |
570
+
571
+ **DOM:** a flex track of `width: max-content` inside an
572
+ `overflow: hidden` viewport. Tracks are found by the `[data-marquee-track]`
573
+ marker (pass one track, or a container and every track inside it is picked up);
574
+ `data-dir="left|right"` sets each row's direction. The `.ak-marquee*` classes
575
+ in the companion stylesheet provide the viewport and its edge masks. `destroy()`
576
+ stops the rAF loop and removes the copy it duplicated (flag + children), so the
577
+ markup matches what you started with — a copy you tiled yourself is left alone.
578
+
579
+ #### `dragStrip(track, options?) => destroy`
580
+
581
+ Infinite draggable carousel (GSAP `Draggable`), with items tilting as you pull
582
+ and springing straight on release.
583
+
584
+ ```ts
585
+ dragStrip("[data-drag]", { maxRotation: 60, rotationScale: 120, settleDuration: 1, inertia: false, clone: true, item: ":scope > *" });
586
+ ```
587
+
588
+ | Option | Default | Notes |
589
+ | ---------------- | --------------- | ------------------------------------------------------- |
590
+ | `maxRotation` | `100` | max tilt in degrees at full drag speed |
591
+ | `rotationScale` | `100` | divisor on the normalised drag speed — higher is subtler |
592
+ | `settleDuration` | `1` | spring-back duration on release, seconds |
593
+ | `inertia` | `false` | throw after release — requires GSAP's `InertiaPlugin` |
594
+ | `clone` | `true` | duplicate content for a seamless loop; `false` clamps |
595
+ | `item` | `":scope > *"` | items inside the strip that tilt |
596
+
597
+ **How the loop works.** Draggable is the *single writer* of the track's X
598
+ transform — a `liveSnap` hook folds every position back into `[-loop, 0]`
599
+ (Draggable has no `modifiers` option; `liveSnap` is the supported place), so
600
+ dragging is 1:1 with the pointer and never stutters between a tween and the
601
+ drag. Folding is only invisible when the content tiles: with `clone: true`
602
+ the strip duplicates itself until one tile is at least as wide as the
603
+ viewport, and `loop` is always a whole multiple of the tile width — the same
604
+ trick `marquee()` uses, so the seam is invisible. With `clone: false` there is
605
+ nothing to fold against, so the strip clamps at the content edges instead
606
+ (finite, but never an empty gap). During an inertia throw, `liveSnap` keeps
607
+ folding each frame while the end target stays raw, so momentum keeps its
608
+ direction.
609
+
610
+ **Rotation** tracks pointer *speed* (normalised to a 60 fps frame so mouse
611
+ and touch event rates feel the same) rather than a raw per-event delta, and is
612
+ driven by two `quickTo` tweens: a fast follow during the drag, then a
613
+ `settleDuration` spring on release. `destroy()` kills both tweens and the
614
+ Draggable, removes the cloned tiles and restores the element's inline
615
+ cursor/user-select/touch-action.
616
+
617
+ **DOM:** an `overflow: hidden` viewport wrapping a flex track of
618
+ `width: max-content` (the effect sets `cursor: grab`, `user-select: none` and
619
+ `touch-action: pan-y` inline and restores them on destroy); items keep
620
+ `transform-origin: 50% 100%` so they pivot from their base.
621
+
622
+ #### `audioBars(target, options?) => handle`
623
+
624
+ Equaliser visualiser — returns `{ start, stop, destroy }`.
625
+
626
+ ```ts
627
+ const eq = audioBars("[data-eq]", { interval: 100, minHeight: 4, maxHeight: 16, bounce: 0.75, bar: ":scope > *" });
628
+ eq.start(); // animate
629
+ eq.stop(); // hold
630
+ eq.destroy();
631
+ ```
632
+
633
+ **DOM:** a row of `<i>` bars — `.ak-eq` in the companion stylesheet. `destroy()`
634
+ clears the interval, kills in-flight bar tweens (otherwise their next frame
635
+ would rewrite `height` *after* teardown) and clears the inline height.
636
+
637
+ ---
638
+
639
+ ### Buttons & links
640
+
641
+ Hover affordances for CTAs and inline links.
642
+
643
+ #### `liquidButton(target, options?) => destroy`
644
+
645
+ SVG wave floods the button on hover.
646
+
647
+ ```ts
648
+ liquidButton("[data-liquid]", { direction: "up", duration: 900, fill: "var(--ak-primary)", labelColor: "#fff" });
649
+ ```
650
+
651
+ **DOM:** the `.ak-liquid` structure (the stylesheet defines the classes; the
652
+ wave path shape is yours to choose):
653
+
654
+ ```html
655
+ <button class="ak-liquid" data-liquid>
656
+ <svg class="ak-liquid__wave" viewBox="0 0 100 100" preserveAspectRatio="none">
657
+ <path d="M0,30 Q50,-5 100,30 L100,100 L0,100 Z" />
658
+ </svg>
659
+ <span class="ak-liquid__label">Lets Talk →</span>
660
+ </button>
661
+ ```
662
+
663
+ The effect stamps each element with `data-ak-liquid="up|down"` plus the
664
+ `--ak-liquid-duration/fill/label` CSS variables (and removes them on destroy).
665
+ Because `direction` applies to every matched element, scope the call for buttons
666
+ that should fill the other way:
667
+
668
+ ```ts
669
+ liquidButton("[data-liquid]", { direction: "up" });
670
+ liquidButton('[data-liquid][data-dir="down"]', { direction: "down" });
671
+ ```
672
+
673
+ #### `underlineLink(target) => destroy`
674
+
675
+ Underline sweep for links — matches `a.ak-underline`.
676
+
677
+ ```ts
678
+ underlineLink("a.ak-underline"); // or any link list
679
+ ```
680
+
681
+ Pure CSS under the hood — it just adds/removes the `.ak-underline` class whose
682
+ `::after` sweep is styled by the companion stylesheet, and `destroy()` removes
683
+ the class again.
684
+
685
+ ---
686
+
687
+ ### Navigation & overlays
688
+
689
+ Page chrome — header behaviour, fullscreen menu, cursor.
690
+
691
+ #### `navHide(nav, options?) => destroy`
692
+
693
+ Header that hides on scroll-down and returns on scroll-up.
694
+
695
+ ```ts
696
+ navHide("[data-nav]", { threshold: 200, hideY: -100, mobileBreakpoint: 768, startHidden: true });
697
+ ```
698
+
699
+ `destroy()` removes the scroll listener, kills any in-flight slide and clears
700
+ the nav's transform — it never starts a *new* animation during teardown.
701
+
702
+ #### `menuOverlay(options) => handle`
703
+
704
+ Full-screen curtain menu — clip-path polygon expands from the bottom edge,
705
+ links stagger in.
706
+
707
+ ```ts
708
+ const menu = menuOverlay({
709
+ overlay: "[data-menu]", // required
710
+ openTrigger: "[data-menu-open], [data-menu-open-2]",
711
+ closeTrigger: "[data-menu-close]",
712
+ nav: "[data-nav]", // slides away while open
713
+ link: ".menu-link a", // default
714
+ chrome: "[data-menu-chrome]", // default
715
+ duration: 1,
716
+ stagger: 0.1,
717
+ initialOpen: false,
718
+ onOpen: () => {}, onClose: () => {},
719
+ });
720
+
721
+ menu.open(); menu.close(); menu.toggle(); menu.isOpen(); menu.destroy();
722
+ ```
723
+
724
+ Curtain uses `EASES.curtain` (`.76,0,.24,1`).
725
+
726
+ #### `cursorFollower(zone, options?) => destroy`
727
+
728
+ Spring-followed cursor tag, e.g. "▶ Play Showreel" over a video.
729
+
730
+ ```ts
731
+ cursorFollower("[data-showreel]", {
732
+ follower: "[data-cursor]", // defaults to the first [data-cursor]
733
+ offset: 14,
734
+ spring: { mass: 0.1, stiffness: 120 },
735
+ blendMode: "exclusion",
736
+ fade: true, // fade in/out with the pointer
737
+ });
738
+ ```
739
+
740
+ `destroy()` kills both spring tweens (they are paused at creation and would
741
+ otherwise live on the global timeline forever) plus any in-flight fade, removes
742
+ the listeners, and restores the inline styles it overwrote.
743
+
744
+ ---
745
+
746
+ ### Intros & transitions
747
+
748
+ Entrance and theme-change moments.
749
+
750
+ #### `preloader(target, options?) => destroy`
751
+
752
+ The 0→100 intro: counter ticks up while an SVG glyph fills via `inset()`
753
+ clip-path, then the glyph scales up and the backdrop fades.
754
+
755
+ ```ts
756
+ preloader("[data-preloader]", {
757
+ glyph: "[data-glyph]", // defaults to the first <svg> inside the root
758
+ counter: "[data-counter]", // defaults to [data-counter] inside the root
759
+ backdrop: "[data-backdrop]", // defaults to [data-backdrop] inside the root
760
+ duration: 4, // seconds
761
+ step: 5, // increment per tick
762
+ interval: 200, // ms
763
+ sessionGuard: true, // skip when already shown this session
764
+ storageKey: "ak-preloader-shown",
765
+ onComplete: () => {},
766
+ });
767
+
768
+ // Options-object form also works:
769
+ preloader({ root: "#preloader", sessionGuard: false });
770
+ ```
771
+
772
+ `destroy()` clears the timers and kills the tweens.
773
+
774
+ #### `themeReveal(options?) => handle`
775
+
776
+ Light/dark toggle with a circular **View Transitions** wipe (graceful fallback
777
+ to an instant swap when the API is missing).
778
+
779
+ ```ts
780
+ const theme = themeReveal({
781
+ toggle: "[data-theme]",
782
+ storageKey: "ak-theme",
783
+ initial: "dark", // defaults to <html>'s current class
784
+ origin: "50% 50%", // or an element to centre the circle on
785
+ duration: 1,
786
+ onChange: (t) => {},
787
+ });
788
+
789
+ theme.set("dark"); theme.toggle(); theme.current(); theme.destroy();
790
+ ```
791
+
792
+ ---
793
+
794
+ ### Logos & SVG
795
+
796
+ Vector reveals for brand marks.
797
+
798
+ #### `logoReveal(svg, options?) => destroy`
799
+
800
+ SVG wordmark assembling letter by letter (staggered `yPercent` + fade).
801
+
802
+ ```ts
803
+ logoReveal("[data-logo]", { path: ".svg-anim-path", stagger: 0.05, once: false });
804
+ // defaults: duration 1, ease "power2.out", start "top 80%", end "bottom top"
805
+ ```
806
+
807
+ **DOM:** `<svg data-logo>` containing paths matching
808
+ `[data-logo-path], .svg-anim-path`.
809
+
810
+ ---
811
+
812
+ ### Utilities
813
+
814
+ ```ts
815
+ toArray(target, scope?) // resolve TargetLike → Element[]
816
+ one(target, scope?) // resolve TargetLike → first Element | null
817
+ onReady(fn) // run after DOMContentLoaded (or immediately)
818
+ compose(...fns) // combine destroy fns → one destroy (skips holes)
819
+ raf(fn) // rAF loop → returns a stop function
820
+ prefersReducedMotion() // boolean, honours matchMedia
821
+ ```
822
+
823
+ Types: `TargetLike`, `Destroy`, `CommonOptions`.
824
+
825
+ ---
826
+
827
+ ## Styling
828
+
829
+ ```ts
830
+ import "anim-kit/styles"; // → dist/styles/anim-kit.css
831
+ ```
832
+
833
+ The companion stylesheet supplies:
834
+
835
+ - **Design tokens:** `--ak-primary`, `--ak-curtain`, `--ak-reveal`, `--ak-out`
836
+ - **Text masks:** `.ak-line-mask`, `.ak-line`, `.ak-word`, `.ak-space`
837
+ - **Heading masks:** `.ak-mask`, `.ak-mask__inner`
838
+ - **Liquid button:** `.ak-liquid`, `.ak-liquid__wave`, `.ak-liquid__label`
839
+ - **Underline:** `.ak-underline`
840
+ - **Marquee:** `.ak-marquee`, `.ak-marquee__viewport`, `.ak-marquee__track`
841
+ - **Drag strip:** `.ak-drag-track`
842
+ - **Menu:** `.menu-overlay`, `.menu-overlay-bar`, `.menu-link`
843
+ - **Card deck:** `.ak-stack-viewport`, `.ak-stack-cards`, `.ak-card`
844
+ - **Misc:** `.ak-counter`, `.ak-eq`
845
+
846
+ Override the tokens to rebrand:
847
+
848
+ ```css
849
+ :root {
850
+ --ak-primary: #ff5c39;
851
+ --ak-curtain: cubic-bezier(0.76, 0, 0.24, 1);
852
+ }
853
+ ```
854
+
855
+ Effects only touch inline styles/transforms; the layout classes above are the
856
+ resting states (safe with reduced motion).
857
+
858
+ ---
859
+
860
+ ## Reduced motion
861
+
862
+ Everything routes through `guard()`:
863
+
864
+ - With `prefers-reduced-motion: reduce`, effects do **not** animate — they snap
865
+ to a safe resting state (e.g. `preloader` hides the overlay, `menuOverlay`
866
+ leaves the curtain collapsed, `smoothScroll` stays inert).
867
+ - Opt a single call out with `force: true`:
868
+
869
+ ```ts
870
+ lineReveal("h1", { force: true }); // animate regardless
871
+ ```
872
+
873
+ Keyboard focus styles (`.ak-liquid:focus-visible`, `.ak-underline:focus-visible`)
874
+ are preserved by the stylesheet.
875
+
876
+ ---
877
+
878
+ ## Framework integration
879
+
880
+ Because each effect is `target + options → destroy`, wiring is mechanical.
881
+
882
+ **React**
883
+
884
+ ```tsx
885
+ useEffect(() => {
886
+ const destroy = lineReveal(ref.current, { mode: "scroll" });
887
+ return destroy; // runs on unmount / StrictMode double-invoke
888
+ }, []);
889
+ ```
890
+
891
+ **Vue**
892
+
893
+ ```ts
894
+ onMounted(() => (destroy = marquee("[data-marquee-track]", { speed: 40 })));
895
+ onBeforeUnmount(() => destroy?.());
896
+ ```
897
+
898
+ **Svelte**
899
+
900
+ ```svelte
901
+ <script lang="ts">
902
+ import { onMount } from "svelte";
903
+ onMount(() => parallax("[data-parallax]")); // returned fn runs on destroy
904
+ </script>
905
+ ```
906
+
907
+ **Next.js (App Router)** — effects touch `window`, so create them in
908
+ `useEffect`; never at module scope.
909
+
910
+ Mount effects **after** content is in the DOM (and after fonts/images if the
911
+ effect measures — `scatterText` waits for `document.fonts.ready` itself), then
912
+ destroy on teardown. Route changes and HMR are why `destroy()` exists.
913
+
914
+ ---
915
+
916
+ ## Demo & tests
917
+
918
+ ```bash
919
+ npm run build # tsc → dist/ (ESM + .d.ts) + CSS copy + tsup standalone bundle
920
+ npm run demo # static server on http://localhost:4321/demo/
921
+ npm test # build + unit smoke + demo integration smoke
922
+ npm run smoke # both smokes (expects dist/ to exist)
923
+ npm run typecheck # tsc --noEmit (what CI runs)
924
+ ```
925
+
926
+ The demo page wires the whole effect set against one document —
927
+ `demo/index.html` + `demo/demo.js`. (`stackedCardsPinned`, the
928
+ sibling-viewport variant, plus `split()` are exercised by the unit smoke
929
+ instead.)
930
+
931
+ ### Copy-prompt API
932
+
933
+ The demo server doubles as a **prompt server**: every effect has a ready-to-
934
+ paste *"how to implement this with anim-kit"* prompt — markup, import,
935
+ initialisation call, options table, teardown and gotchas:
936
+
937
+ ```bash
938
+ curl http://localhost:4321/api/prompts # { count, categories, prompts: [{ id, title, summary, category, subcategory, text }] }
939
+ curl http://localhost:4321/api/prompts/marquee # one prompt, text/plain
940
+ ```
941
+
942
+ On the page, every labelled section carries a **copy prompt** chip, and the
943
+ floating **⧉ prompts (22)** button at the bottom right opens the full
944
+ catalogue grouped by [effect category](#effect-categories) — one click copies
945
+ an effect's prompt (the prompt states its category), *copy all* puts the
946
+ entire set on the clipboard. The catalogue lives in `scripts/prompts.mjs`:
947
+ one entry per effect rendered by `renderPrompt()`, plus the `TAXONOMY` tree
948
+ that classifies every effect. Adding a prompt is a matter of adding an entry
949
+ and slotting the effect into a subcategory.
950
+
951
+ **Unit smoke** (`scripts/smoke.mjs`) runs the built bundle in **jsdom** and
952
+ asserts:
953
+
954
+ 1. all 38 exports are present;
955
+ 2. plugins (`ScrollTrigger`, `SplitText`, `Draggable`, `CustomEase`,
956
+ `ScrollSmoother`) and the 4 custom eases are registered;
957
+ 3. every effect no-ops safely on missing targets;
958
+ 4. 16 effects mount on real markup and unmount cleanly;
959
+ 5. `preloader` ticks in both the positional and options-object call forms;
960
+ 6. `lineReveal` actually splits into masked lines and restores markup on
961
+ destroy;
962
+ 7. `utils`, `compose` and `guard` behave per contract.
963
+
964
+ **Demo smoke** (`scripts/demo-smoke.mjs`) loads the real `demo/index.html` and
965
+ executes the real `demo/demo.js` wiring against it, then asserts the effects
966
+ actually *did* something (hero split, preloader counter ticking, marquee track
967
+ duplicated, per-call liquid directions, menu/theme/smooth-scroll handles in
968
+ their initial state), that ~40 ScrollTriggers + a Draggable were created, that
969
+ no console errors were logged, that `dragStrip` tiled its content for the
970
+ seamless loop, that every effect referenced on the page has a `/api/prompts`
971
+ entry, that the taxonomy classifies every effect exactly once, that the
972
+ prompt dock renders one group per category with every effect listed once,
973
+ and that teardown leaves **zero** live ScrollTriggers, Draggables or
974
+ page-element tweens behind while restoring the original markup (marquee and
975
+ drag-strip clones removed).
976
+
977
+ > jsdom is used deliberately: GSAP's CSSPlugin/Draggable probe element
978
+ > style/computed values during registration, which a hand-rolled DOM stub
979
+ > cannot satisfy. Shared environment shims live in `scripts/env.mjs`.
980
+
981
+ ---
982
+
983
+ ## Project structure
984
+
985
+ ```
986
+ anim-kit/
987
+ ├─ src/
988
+ │ ├─ core/
989
+ │ │ ├─ gsap.ts initGSAP() + EASES (single source of truth)
990
+ │ │ ├─ split.ts SplitText wrapper + manual fallback
991
+ │ │ ├─ smooth-scroll.ts Lenis ↔ ScrollTrigger bridge
992
+ │ │ ├─ guard.ts reduced-motion gate
993
+ │ │ ├─ util.ts toArray/one/onReady/compose/raf
994
+ │ │ └─ types.ts TargetLike / Destroy / CommonOptions
995
+ │ ├─ effects/ one file per effect (17 files, 21 effect functions)
996
+ │ ├─ styles/anim-kit.css companion stylesheet
997
+ │ └─ index.ts barrel — 38 exports
998
+ ├─ demo/ visual demo (import map, no bundler)
999
+ ├─ scripts/
1000
+ │ ├─ serve.mjs static server + /api/prompts (:4321)
1001
+ │ ├─ prompts.mjs prompt catalogue + effect TAXONOMY → /api/prompts
1002
+ │ ├─ copy-assets.mjs copies CSS into dist/
1003
+ │ ├─ env.mjs shared jsdom shims (matchMedia, scrollTo, rAF, …)
1004
+ │ ├─ smoke.mjs unit smoke test (incl. standalone API parity)
1005
+ │ └─ demo-smoke.mjs runs the real demo wiring against real markup
1006
+ ├─ tsup.config.ts bundles dist/anim-kit.standalone.js (the CDN entry)
1007
+ ├─ LICENSE MIT
1008
+ └─ dist/ build output
1009
+ ├─ index.js / *.d.ts per-file ESM + declarations (tsc)
1010
+ ├─ effects/*.js one module per effect → anim-kit/effects/* subpaths
1011
+ ├─ anim-kit.standalone.js self-contained CDN bundle (gsap+lenis inlined)
1012
+ └─ styles/anim-kit.css plain CSS, copied verbatim
1013
+ ```
1014
+
1015
+ CI and release workflows live at the repository root:
1016
+ `.github/workflows/ci.yml` (type-check + build + smoke + pack check on every
1017
+ push/PR) and `.github/workflows/release.yml` (tag `v*` → `npm publish
1018
+ --provenance`, needs the `NPM_TOKEN` repo secret).
1019
+
1020
+ Each effect is an independent module — if you only need the marquee, import
1021
+ `marquee` and the bundler drops the rest, or deep-import
1022
+ `anim-kit/effects/marquee` to skip the barrel entirely.
1023
+
1024
+ ---
1025
+
1026
+ ## Credits
1027
+
1028
+ Effects extracted and reimplemented from the animation patterns of
1029
+ [dzinrstudio.com](https://dzinrstudio.com/). Built on
1030
+ [GSAP](https://gsap.com/) (free plugins only) and
1031
+ [Lenis](https://lenis.darkroom.engineering/).
1032
+
1033
+ MIT © Kalakriti