@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.
- package/LICENSE +21 -0
- package/README.md +1033 -0
- package/dist/anim-kit.standalone.js +117 -0
- package/dist/anim-kit.standalone.js.map +1 -0
- package/dist/core/gsap.d.ts +44 -0
- package/dist/core/gsap.d.ts.map +1 -0
- package/dist/core/gsap.js +71 -0
- package/dist/core/gsap.js.map +1 -0
- package/dist/core/guard.d.ts +3 -0
- package/dist/core/guard.d.ts.map +1 -0
- package/dist/core/guard.js +18 -0
- package/dist/core/guard.js.map +1 -0
- package/dist/core/smooth-scroll.d.ts +45 -0
- package/dist/core/smooth-scroll.d.ts.map +1 -0
- package/dist/core/smooth-scroll.js +98 -0
- package/dist/core/smooth-scroll.js.map +1 -0
- package/dist/core/split.d.ts +24 -0
- package/dist/core/split.d.ts.map +1 -0
- package/dist/core/split.js +117 -0
- package/dist/core/split.js.map +1 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/util.d.ts +19 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +73 -0
- package/dist/core/util.js.map +1 -0
- package/dist/effects/counter.d.ts +52 -0
- package/dist/effects/counter.d.ts.map +1 -0
- package/dist/effects/counter.js +106 -0
- package/dist/effects/counter.js.map +1 -0
- package/dist/effects/cursor-follower.d.ts +19 -0
- package/dist/effects/cursor-follower.d.ts.map +1 -0
- package/dist/effects/cursor-follower.js +76 -0
- package/dist/effects/cursor-follower.js.map +1 -0
- package/dist/effects/drag-strip.d.ts +17 -0
- package/dist/effects/drag-strip.d.ts.map +1 -0
- package/dist/effects/drag-strip.js +186 -0
- package/dist/effects/drag-strip.js.map +1 -0
- package/dist/effects/hero-shrink.d.ts +15 -0
- package/dist/effects/hero-shrink.d.ts.map +1 -0
- package/dist/effects/hero-shrink.js +40 -0
- package/dist/effects/hero-shrink.js.map +1 -0
- package/dist/effects/horizontal-scroll.d.ts +17 -0
- package/dist/effects/horizontal-scroll.d.ts.map +1 -0
- package/dist/effects/horizontal-scroll.js +87 -0
- package/dist/effects/horizontal-scroll.js.map +1 -0
- package/dist/effects/line-reveal.d.ts +34 -0
- package/dist/effects/line-reveal.d.ts.map +1 -0
- package/dist/effects/line-reveal.js +71 -0
- package/dist/effects/line-reveal.js.map +1 -0
- package/dist/effects/liquid-button.d.ts +19 -0
- package/dist/effects/liquid-button.d.ts.map +1 -0
- package/dist/effects/liquid-button.js +52 -0
- package/dist/effects/liquid-button.js.map +1 -0
- package/dist/effects/logo-reveal.d.ts +15 -0
- package/dist/effects/logo-reveal.d.ts.map +1 -0
- package/dist/effects/logo-reveal.js +49 -0
- package/dist/effects/logo-reveal.js.map +1 -0
- package/dist/effects/marquee.d.ts +13 -0
- package/dist/effects/marquee.d.ts.map +1 -0
- package/dist/effects/marquee.js +143 -0
- package/dist/effects/marquee.js.map +1 -0
- package/dist/effects/mask-reveal.d.ts +36 -0
- package/dist/effects/mask-reveal.d.ts.map +1 -0
- package/dist/effects/mask-reveal.js +102 -0
- package/dist/effects/mask-reveal.js.map +1 -0
- package/dist/effects/menu-overlay.d.ts +32 -0
- package/dist/effects/menu-overlay.d.ts.map +1 -0
- package/dist/effects/menu-overlay.js +150 -0
- package/dist/effects/menu-overlay.js.map +1 -0
- package/dist/effects/nav-hide.d.ts +13 -0
- package/dist/effects/nav-hide.d.ts.map +1 -0
- package/dist/effects/nav-hide.js +68 -0
- package/dist/effects/nav-hide.js.map +1 -0
- package/dist/effects/parallax.d.ts +11 -0
- package/dist/effects/parallax.d.ts.map +1 -0
- package/dist/effects/parallax.js +48 -0
- package/dist/effects/parallax.js.map +1 -0
- package/dist/effects/preloader.d.ts +33 -0
- package/dist/effects/preloader.d.ts.map +1 -0
- package/dist/effects/preloader.js +128 -0
- package/dist/effects/preloader.js.map +1 -0
- package/dist/effects/scatter-text.d.ts +20 -0
- package/dist/effects/scatter-text.d.ts.map +1 -0
- package/dist/effects/scatter-text.js +87 -0
- package/dist/effects/scatter-text.js.map +1 -0
- package/dist/effects/stacked-cards.d.ts +24 -0
- package/dist/effects/stacked-cards.d.ts.map +1 -0
- package/dist/effects/stacked-cards.js +106 -0
- package/dist/effects/stacked-cards.js.map +1 -0
- package/dist/effects/theme-reveal.d.ts +35 -0
- package/dist/effects/theme-reveal.d.ts.map +1 -0
- package/dist/effects/theme-reveal.js +93 -0
- package/dist/effects/theme-reveal.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/styles/anim-kit.css +361 -0
- package/package.json +80 -0
- package/src/core/gsap.ts +79 -0
- package/src/core/guard.ts +18 -0
- package/src/core/smooth-scroll.ts +143 -0
- package/src/core/split.ts +145 -0
- package/src/core/types.ts +30 -0
- package/src/core/util.ts +79 -0
- package/src/effects/counter.ts +192 -0
- package/src/effects/cursor-follower.ts +104 -0
- package/src/effects/drag-strip.ts +228 -0
- package/src/effects/hero-shrink.ts +69 -0
- package/src/effects/horizontal-scroll.ts +123 -0
- package/src/effects/line-reveal.ts +109 -0
- package/src/effects/liquid-button.ts +75 -0
- package/src/effects/logo-reveal.ts +76 -0
- package/src/effects/marquee.ts +157 -0
- package/src/effects/mask-reveal.ts +148 -0
- package/src/effects/menu-overlay.ts +218 -0
- package/src/effects/nav-hide.ts +90 -0
- package/src/effects/parallax.ts +68 -0
- package/src/effects/preloader.ts +187 -0
- package/src/effects/scatter-text.ts +129 -0
- package/src/effects/stacked-cards.ts +154 -0
- package/src/effects/theme-reveal.ts +144 -0
- package/src/index.ts +98 -0
- 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
|