@cosmictraveler002/anim-kit 1.4.0 → 1.6.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 (56) hide show
  1. package/README.md +256 -23
  2. package/dist/anim-kit.standalone.js +5 -5
  3. package/dist/anim-kit.standalone.js.map +1 -1
  4. package/dist/core/smooth-scroll.d.ts +29 -1
  5. package/dist/core/smooth-scroll.d.ts.map +1 -1
  6. package/dist/core/smooth-scroll.js +23 -3
  7. package/dist/core/smooth-scroll.js.map +1 -1
  8. package/dist/effects/horizontal-scroll.d.ts.map +1 -1
  9. package/dist/effects/horizontal-scroll.js +3 -0
  10. package/dist/effects/horizontal-scroll.js.map +1 -1
  11. package/dist/effects/ink-wipe.d.ts +41 -0
  12. package/dist/effects/ink-wipe.d.ts.map +1 -0
  13. package/dist/effects/ink-wipe.js +342 -0
  14. package/dist/effects/ink-wipe.js.map +1 -0
  15. package/dist/effects/scatter-text.d.ts.map +1 -1
  16. package/dist/effects/scatter-text.js +3 -0
  17. package/dist/effects/scatter-text.js.map +1 -1
  18. package/dist/effects/stacked-cards.d.ts.map +1 -1
  19. package/dist/effects/stacked-cards.js +3 -0
  20. package/dist/effects/stacked-cards.js.map +1 -1
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/three/coverflow-wheel.d.ts +26 -0
  26. package/dist/three/coverflow-wheel.d.ts.map +1 -0
  27. package/dist/three/coverflow-wheel.js +495 -0
  28. package/dist/three/coverflow-wheel.js.map +1 -0
  29. package/dist/three/dither-reveal.d.ts +36 -0
  30. package/dist/three/dither-reveal.d.ts.map +1 -0
  31. package/dist/three/dither-reveal.js +292 -0
  32. package/dist/three/dither-reveal.js.map +1 -0
  33. package/dist/three/index.d.ts +8 -0
  34. package/dist/three/index.d.ts.map +1 -1
  35. package/dist/three/index.js +4 -0
  36. package/dist/three/index.js.map +1 -1
  37. package/dist/three/tear-reveal.d.ts +54 -0
  38. package/dist/three/tear-reveal.d.ts.map +1 -0
  39. package/dist/three/tear-reveal.js +269 -0
  40. package/dist/three/tear-reveal.js.map +1 -0
  41. package/dist/three/wordmark-wave.d.ts +29 -0
  42. package/dist/three/wordmark-wave.d.ts.map +1 -0
  43. package/dist/three/wordmark-wave.js +461 -0
  44. package/dist/three/wordmark-wave.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/core/smooth-scroll.ts +44 -2
  47. package/src/effects/horizontal-scroll.ts +3 -0
  48. package/src/effects/ink-wipe.ts +409 -0
  49. package/src/effects/scatter-text.ts +3 -0
  50. package/src/effects/stacked-cards.ts +3 -0
  51. package/src/index.ts +3 -0
  52. package/src/three/coverflow-wheel.ts +569 -0
  53. package/src/three/dither-reveal.ts +365 -0
  54. package/src/three/index.ts +8 -0
  55. package/src/three/tear-reveal.ts +351 -0
  56. package/src/three/wordmark-wave.ts +517 -0
package/README.md CHANGED
@@ -30,6 +30,7 @@ destroy();
30
30
  - [Core](#core)
31
31
  - [Text animations](#text-animations)
32
32
  - [Scroll & media](#scroll--media)
33
+ - [WebGL](#webgl)
33
34
  - [Loops & marquees](#loops--marquees)
34
35
  - [Buttons & links](#buttons--links)
35
36
  - [Navigation & overlays](#navigation--overlays)
@@ -78,7 +79,7 @@ inside the published tarball):
78
79
 
79
80
  | Specifier | Resolves to | Use for |
80
81
  |---|---|---|
81
- | `@cosmictraveler002/anim-kit` | `dist/index.js` + `dist/index.d.ts` | the full barrel — 47 exports |
82
+ | `@cosmictraveler002/anim-kit` | `dist/index.js` + `dist/index.d.ts` | the full barrel — 48 exports |
82
83
  | `@cosmictraveler002/anim-kit/effects/<name>` | `dist/effects/<name>.js` + `.d.ts` | one effect in isolation (`marquee`, `lineReveal`, …) |
83
84
  | `@cosmictraveler002/anim-kit/three` | `dist/three/index.js` + `.d.ts` | WebGL effects — needs the optional `three` peer (never a core dep) |
84
85
  | `@cosmictraveler002/anim-kit/standalone` | `dist/anim-kit.standalone.js` (types → `index.d.ts`) | the self-contained bundle — same API |
@@ -98,7 +99,7 @@ resolves declarations through the same map — no `typesVersions` shim needed.
98
99
 
99
100
  No build step on the consumer's end: `dist/` is served as-is from the npm
100
101
  tarball by any npm CDN. Every URL is **version-pinned** — npm versions are
101
- immutable, so `@cosmictraveler002/anim-kit@1.4.0` always resolves to exactly that build, forever
102
+ immutable, so `@cosmictraveler002/anim-kit@1.6.0` always resolves to exactly that build, forever
102
103
  (only a new version creates a new URL; nothing floats unless you ask for a
103
104
  range).
104
105
 
@@ -109,12 +110,12 @@ plugins anim-kit uses) and `lenis` **inlined** — no import map, one URL, works
109
110
  identically on jsDelivr and unpkg:
110
111
 
111
112
  ```html
112
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/styles/anim-kit.css" />
113
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.6.0/dist/styles/anim-kit.css" />
113
114
 
114
115
  <script type="module">
115
116
  import {
116
117
  smoothScroll, lineReveal, marquee,
117
- } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/anim-kit.standalone.js";
118
+ } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.6.0/dist/anim-kit.standalone.js";
118
119
 
119
120
  smoothScroll();
120
121
  lineReveal("[data-lines]", { mode: "scroll" });
@@ -122,7 +123,7 @@ identically on jsDelivr and unpkg:
122
123
  </script>
123
124
  ```
124
125
 
125
- unpkg serves the same file: `https://unpkg.com/@cosmictraveler002/anim-kit@1.4.0/dist/anim-kit.standalone.js`
126
+ unpkg serves the same file: `https://unpkg.com/@cosmictraveler002/anim-kit@1.6.0/dist/anim-kit.standalone.js`
126
127
 
127
128
  ### Option 2 — jsDelivr `+esm`
128
129
 
@@ -131,7 +132,7 @@ per version):
131
132
 
132
133
  ```html
133
134
  <script type="module">
134
- import { lineReveal } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/+esm";
135
+ import { lineReveal } from "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.6.0/+esm";
135
136
  </script>
136
137
  ```
137
138
 
@@ -143,13 +144,13 @@ locally, with CDN URLs — and the way to share one GSAP between anim-kit and
143
144
  the rest of your page:
144
145
 
145
146
  ```html
146
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/styles/anim-kit.css" />
147
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.6.0/dist/styles/anim-kit.css" />
147
148
 
148
149
  <script type="importmap">
149
150
  {
150
151
  "imports": {
151
- "@cosmictraveler002/anim-kit": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/index.js",
152
- "@cosmictraveler002/anim-kit/three": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.4.0/dist/three/index.js",
152
+ "@cosmictraveler002/anim-kit": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.6.0/dist/index.js",
153
+ "@cosmictraveler002/anim-kit/three": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.6.0/dist/three/index.js",
153
154
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
154
155
  "gsap": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/index.js",
155
156
  "gsap/ScrollTrigger": "https://cdn.jsdelivr.net/npm/gsap@3.15.0/ScrollTrigger.js",
@@ -170,7 +171,7 @@ the rest of your page:
170
171
  </script>
171
172
  ```
172
173
 
173
- Swap the host for unpkg (`https://unpkg.com/@cosmictraveler002/anim-kit@1.4.0/dist/index.js`, …) —
174
+ Swap the host for unpkg (`https://unpkg.com/@cosmictraveler002/anim-kit@1.6.0/dist/index.js`, …) —
174
175
  the file layout is identical. GSAP subpaths are listed one by one because
175
176
  import maps match specifiers literally: a trailing-slash prefix map would
176
177
  produce extension-less URLs, which CDNs don't serve. The `gsap`/`lenis` pins
@@ -276,8 +277,12 @@ configured before first paint.
276
277
  ## Smooth scroll
277
278
 
278
279
  Lenis is wired to ScrollTrigger the canonical way: Lenis drives the scroll,
279
- `lenis.on("scroll", ScrollTrigger.update)` keeps triggers in sync, `lenis.raf`
280
- is ticked from `gsap.ticker`, and `lagSmoothing(0)` is disabled.
280
+ `lenis.on("scroll", ScrollTrigger.update)` keeps triggers in sync, and
281
+ `lenis.raf` is ticked from `gsap.ticker` so scroll and tweens run off one
282
+ clock. GSAP's lag smoothing stays **on**: ticker listeners receive the same
283
+ adjusted clock the tweens use, so a stalled frame (GC pause, buffer tick)
284
+ advances one short step everywhere instead of snapping the scroll and every
285
+ scroll-linked tween to a new time in a single jump.
281
286
 
282
287
  ```ts
283
288
  const scroller = smoothScroll({
@@ -287,6 +292,11 @@ const scroller = smoothScroll({
287
292
  orientation: "vertical",
288
293
  initialScroll: 0,
289
294
  useScrollerProxy: false, // opt-in scrollerProxy for nested scrollers
295
+ touchMultiplier: 1, // touch-drag sensitivity (Lenis default 1)
296
+ wheelMultiplier: 1, // mouse-wheel sensitivity (Lenis default 1)
297
+ // Lag smoothing: leave unset for GSAP's built-in 500ms → 33ms compensation,
298
+ // false to disable it (the old behaviour), or retune for short hitches:
299
+ lagSmoothing: { threshold: 150 },
290
300
  });
291
301
 
292
302
  scroller.scrollTo("#work", { duration: 1.2 }); // programmatic, smoothed
@@ -320,7 +330,9 @@ prompt dock, and backs the `category` / `subcategory` fields on
320
330
  | Scroll & media | Parallax & depth | `parallax` |
321
331
  | Scroll & media | Heroes & media | `heroShrink`, `mediaSettle` |
322
332
  | Scroll & media | Enter reveals | `revealRule`, `unfoldReveal`, `clipWipe` |
323
- | WebGL | Shader media | `webglMedia`, `glRail` |
333
+ | WebGL | Shader media | `webglMedia`, `glRail`, `coverflowWheel` |
334
+ | WebGL | Scenes & overlays | `tearReveal`, `ditherReveal` |
335
+ | WebGL | Kinetic type | `wordmarkWave` |
324
336
  | Loops & marquees | Marquees | `marquee` |
325
337
  | Loops & marquees | Draggables & rails | `dragStrip`, `dragRail` |
326
338
  | Loops & marquees | Equalizers | `audioBars` |
@@ -331,6 +343,7 @@ prompt dock, and backs the `category` / `subcategory` fields on
331
343
  | Navigation & overlays | Cursors | `cursorFollower` |
332
344
  | Intros & transitions | Preloaders | `preloader` |
333
345
  | Intros & transitions | Theme wipes | `themeReveal` |
346
+ | Intros & transitions | Page transitions | `inkWipe` |
334
347
  | Logos & SVG | Path reveals | `logoReveal` |
335
348
 
336
349
  **Growing the library:** a subcategory is the slot sibling effects land in —
@@ -797,7 +810,8 @@ the entrance.
797
810
 
798
811
  ### WebGL
799
812
 
800
- GPU media — three.js re-renders your images as shader cards. This is the only
813
+ GPU media — three.js re-renders your content as shader cards, kinetic type
814
+ and reveal scenes. This is the only
801
815
  category behind the `@cosmictraveler002/anim-kit/three` subpath: `three` is an
802
816
  **optional peer** (see [Subpath exports](#subpath-exports)), so the core
803
817
  barrel and the standalone bundle stay three-free.
@@ -911,6 +925,183 @@ user-select: none`.
911
925
 
912
926
  ---
913
927
 
928
+ #### `coverflowWheel(target, options?) => handle`
929
+
930
+ Media cards on a flattened vertical wheel: each card bends along the arc in
931
+ the vertex shader instead of staying a rigid plane, fronts face you bright
932
+ while the backs show mirrored, one mip blurrier and half-lit, and the strip
933
+ dissolves into the floor below instead of ending on a hard edge. Drag to
934
+ scrub — a release within 5 % of a slot commits to the neighbouring card,
935
+ smaller drags spring back — and while idle the wheel creeps a slot's lead-in
936
+ and then advances on its own.
937
+
938
+ ```ts
939
+ import { coverflowWheel } from "@cosmictraveler002/anim-kit/three";
940
+
941
+ const wheel = coverflowWheel("#deck", { idle: 5 });
942
+ wheel.goTo(3); // jump to a card (wraps)
943
+ wheel.next(); // one card on, 0.5 s
944
+ wheel.destroy();
945
+ ```
946
+
947
+ | Option | Default | Notes |
948
+ | --- | --- | --- |
949
+ | `index` | `0` | first card on show (wraps) |
950
+ | `autoplay` / `idle` | `true` / `5` | idle advance — seconds of stillness (with a 1/8-slot lead-in creep) |
951
+ | `duration` | `1.15` | `goTo()` tween, seconds (`next()` / `prev()` use 0.5) |
952
+ | `margin` | `0.1` | gap between cards, as a fraction of one slot |
953
+ | `dpr` | `2` | device-pixel-ratio cap |
954
+
955
+ Cards are the host's own `<img>`s — cover-cropped into textures at mount (a
956
+ failed decode drops that card; fewer than two survivors → inert) and restored
957
+ from their inline opacity on teardown. Pointer capture keeps the drag over the
958
+ deck wherever the pointer is and native image-dragging is blocked so a card
959
+ never swallows the stream; the host claims `cursor: grab` + `touch-action:
960
+ pan-y` and a `data-ak-coverflow` stamp, all restored by `destroy()`. Card
961
+ backs reuse the front texture (mirrored, half light, bottom-faded in-shader) —
962
+ no second texture, no extra pass.
963
+
964
+ **Silent no-op ladder.** Missing target → inert handle; no
965
+ `WebGLRenderingContext` (SSR, jsdom, WebGL disabled) → inert *before* any
966
+ context probe; renderer refused → inert; reduced motion → inert, nothing
967
+ mounts. Every call on an inert handle is a safe no-op — nothing ever logs.
968
+ `destroy()` kills the tween + ticker, disconnects the observers, removes the
969
+ canvas and restores the images, host styles and dataset.
970
+
971
+ ---
972
+
973
+ #### `tearReveal(target, options?) => destroy`
974
+
975
+ A torn-edge sheet that sweeps a section in (or out): a noise-displaced
976
+ boundary travels across the overlay — the fbm joins the field *before*
977
+ thresholding, so patches tear away ahead of the front and ride across as
978
+ separate shards — while a barrel warp bows the edge into an arriving curve
979
+ instead of a straight line wobbling in place.
980
+
981
+ ```ts
982
+ tearReveal("[data-tear]", {
983
+ color: ["#ff8a1e", "#ff5252"], // [foot, top] gradient stops (or one colour)
984
+ blend: "multiply", // tint the section instead of covering it
985
+ direction: "up", // up | down | left | right
986
+ mode: "scroll", // play on enter — or "immediate"
987
+ duration: 1, // seconds for the timed run
988
+ });
989
+ // …or bind it to scroll instead:
990
+ tearReveal("[data-tall]", { scrub: 0.4, start: "top 90%", end: "top 30%" });
991
+ tearReveal(document.body, { fixed: true }); // page-sized sweep over the viewport
992
+ ```
993
+
994
+ | Option | Default | Notes |
995
+ | --- | --- | --- |
996
+ | `color` | `"#111111"` | one colour or `[foot, top]` gradient stops |
997
+ | `blend` | `"none"` | CSS `mix-blend-mode` — `multiply` tints what is under the sheet |
998
+ | `direction` | `"up"` | `up` / `down` / `left` / `right` |
999
+ | `dispAmp` / `dispScale` / `dispDetail` / `dispDrift` | `0.175` / `12.2` / `5` / `0` | tear shape — how far, how big, how gritty, how fast it crawls |
1000
+ | `soft` | `0.001` | threshold half-width — small keeps shards crisp |
1001
+ | `lens` | `-0.275` | barrel warp — negative bows the middle of the boundary up |
1002
+ | `duration` / `ease` | `1` / `"power2.inOut"` | timed run (`mode: "scroll"` or `"immediate"`) |
1003
+ | `scrub` | — | smoothing seconds (or `true`) — progress follows the scroll, both ways |
1004
+ | `start` / `end` | `"top 85%"` / `"top 25%"` | ScrollTrigger positions |
1005
+ | `replay` | `false` | re-run on every viewport entry |
1006
+ | `fixed` / `z` / `dpr` | `false` / `1` / `2` | viewport-sized canvas, stacking, pixel-ratio cap |
1007
+
1008
+ Both ends of the travel are **sealed**: the displaced boundary reaches the
1009
+ far side with its deepest tear at exactly zero, so the last half-covered
1010
+ pixels never read as speckle or leftover haze.
1011
+
1012
+ **Silent no-op ladder.** Missing target → no-op; no `WebGLRenderingContext`
1013
+ (SSR, jsdom, WebGL disabled) → no-op *before* any context probe; renderer
1014
+ refused → no-op; reduced motion → nothing mounts. Nothing ever logs.
1015
+ `destroy()` kills the tween + ScrollTrigger, removes the canvas and restores
1016
+ the host's styles.
1017
+
1018
+ **DOM.** `position: relative; overflow: hidden` on the target — or skip the
1019
+ target box entirely with `fixed: true` for a page-sized sweep.
1020
+
1021
+ ---
1022
+
1023
+ #### `ditherReveal(target, options?) => destroy`
1024
+
1025
+ A photo punches through its placeholder in a hard-edged, fbm-speckled ring
1026
+ expanding from the centre outward — no soft dissolve, no crossfade: every
1027
+ pixel on the boundary flips between hole and not-hole. The noise spread
1028
+ ramps to zero at both ends of the travel, so progress 0 is exactly empty
1029
+ and progress 1 exactly solid.
1030
+
1031
+ ```ts
1032
+ ditherReveal("[data-reveal]", {
1033
+ plate: "#14140f", // flat placeholder where the mask has not opened yet
1034
+ duration: 1.15, // seconds, ease "power4.out"
1035
+ maskScale: 50, // fbm frequency across the element — lump scale of the edge
1036
+ hard: true, // the signature speckle (false = a soft dissolve)
1037
+ play: "visible", // fire on first viewport entry ("load" = when the texture decodes)
1038
+ });
1039
+ ```
1040
+
1041
+ | Option | Default | Notes |
1042
+ | --- | --- | --- |
1043
+ | `src` | first `<img>` | image source override when the wrapper has no image |
1044
+ | `duration` / `ease` | `1.15` / `"power4.out"` | the reveal clock |
1045
+ | `plate` | — | flat placeholder colour (omit → the wrapper's own background shows through) |
1046
+ | `maskScale` / `spread` / `detail` | `50` / `0.35` / `4` | edge shape — frequency, reach, fbm octaves |
1047
+ | `hard` | `true` | hard speckle vs soft dissolve |
1048
+ | `play` | `"visible"` | `"visible"` fires on first viewport entry, `"load"` on texture decode |
1049
+ | `dpr` | `2` | device-pixel-ratio cap |
1050
+
1051
+ The `<img>` keeps layout and alt text: the canvas covers the wrapper and
1052
+ only takes over its pixels once the texture has decoded — with a plate the
1053
+ placeholder covers from load, without one the image only steps aside when
1054
+ the reveal actually starts. The cover-crop runs in-shader (`object-fit:
1055
+ cover` maths), so any source aspect fills any card aspect.
1056
+
1057
+ **Silent no-op ladder.** Missing target → no-op; no `WebGLRenderingContext`
1058
+ (SSR, jsdom, WebGL disabled) → no-op *before* any context probe; renderer
1059
+ refused → no-op; texture 404 → the plain `<img>` stays untouched; reduced
1060
+ motion → the canvas never mounts and the static image is the resting state.
1061
+ Nothing ever logs. `destroy()` kills the tween + observers, disposes
1062
+ geometry/material/texture/renderer, removes the canvas and restores the
1063
+ image opacity and wrapper position.
1064
+
1065
+ ---
1066
+
1067
+ #### `wordmarkWave(target, options?) => destroy`
1068
+
1069
+ The target's own headline rendered to a texture and simulated as a spring
1070
+ lattice: the pointer shears the letters like thick liquid, red and cyan split
1071
+ out of the colour while the surface deforms, and every vertex springs back to
1072
+ exact rest. Text, font and colour come from the element itself — nothing to
1073
+ export, nothing to align.
1074
+
1075
+ ```ts
1076
+ import { wordmarkWave } from "@cosmictraveler002/anim-kit/three";
1077
+
1078
+ const destroy = wordmarkWave("#mark", {
1079
+ drag: 1, // pointer-velocity term — how hard the letters smear
1080
+ push: 0, // radial push — vertices flee the cursor while it sits still
1081
+ chroma: true, // red/cyan fringes while deforming
1082
+ });
1083
+ ```
1084
+
1085
+ | Option | Default | Notes |
1086
+ | --- | --- | --- |
1087
+ | `text` / `font` / `color` | target's own | override what is drawn (the host's colour is hidden while mounted) |
1088
+ | `outline` / `tracking` | `0` / `0` | stroke width, px (0 = filled) and extra letter spacing, px |
1089
+ | `drag` / `push` | `1` / `0` | pointer-velocity and radial-push terms of the simulation |
1090
+ | `chroma` | `true` | red/cyan chromatic split while deforming |
1091
+ | `segments` | `0` | grid columns — auto (≈10 px cells, ~5 k vertices) |
1092
+ | `opacity` / `z` / `dpr` | `1` / `1` / `2` | canvas opacity, stacking order, pixel-ratio cap |
1093
+
1094
+ The simulation steps at a fixed 60 Hz (max 4 steps per frame; a gap over
1095
+ 0.2 s counts as one step) so a backgrounded tab resumes where it left instead
1096
+ of exploding, and the ticker idles while the host is off-screen. **Silent
1097
+ no-op ladder**: missing target → no-op; no `WebGLRenderingContext` (SSR,
1098
+ jsdom, WebGL disabled) → no-op *before* any context probe; renderer refused →
1099
+ no-op; reduced motion → nothing mounts, the real text simply stays. Nothing
1100
+ ever logs. `destroy()` stops the ticker + observers, disposes the renderer,
1101
+ removes the canvas and restores the host colour it hid.
1102
+
1103
+ ---
1104
+
914
1105
  ### Loops & marquees
915
1106
 
916
1107
  Continuous motion — marquees, draggable rails, equaliser bars.
@@ -1236,6 +1427,46 @@ theme.set("dark"); theme.toggle(); theme.current(); theme.destroy();
1236
1427
 
1237
1428
  ---
1238
1429
 
1430
+ #### `inkWipe(options?) => handle`
1431
+
1432
+ A brush-loaded ink sheet sweeps across the whole page: `cover()` paints it
1433
+ over, the swap happens at full cover, `unveil()` lets the new page arrive
1434
+ behind the *same* stroke — one continuous left→right travel whose edge is
1435
+ seeded, so it never boils between frames. Bristle spurs and flecks run
1436
+ ahead of the front.
1437
+
1438
+ ```ts
1439
+ const wipe = inkWipe({ links: "[data-wipe]" }); // intercepts links; next page auto-unveils
1440
+
1441
+ await wipe.cover(); // resolves at full cover — swap your DOM here
1442
+ await wipe.unveil(); // the new page arrives behind the same stroke
1443
+ wipe.destroy();
1444
+
1445
+ // Options-object form or target-first form both work:
1446
+ inkWipe({ host: "#overlay", color: "#111" });
1447
+ inkWipe(document.body, { coverMs: 700 });
1448
+ ```
1449
+
1450
+ | Option | Default | Notes |
1451
+ | --- | --- | --- |
1452
+ | `host` | `document.body` | overlay host the canvas is appended to (target arg also works) |
1453
+ | `color` | `"#0b0b0b"` | ink colour |
1454
+ | `coverMs` / `unveilMs` | `550` / `650` | sweep durations |
1455
+ | `ease` | `"power2.inOut"` | GSAP ease for both halves |
1456
+ | `tilt` / `rough` / `bristle` | `0.35` / `44` / `180` | edge lean (× height), noise amplitude px, longest spur px |
1457
+ | `links` | — | selectors intercepted — a click runs the wipe before navigating |
1458
+ | `sessionKey` / `autoUnveil` | `"ak-ink-wipe"` / `true` | cross-page handoff flag + the page-load unveil |
1459
+ | `z` | `9998` | overlay z-index |
1460
+
1461
+ **The MPA handoff** is a sessionStorage flag: a covered navigation stores
1462
+ `sessionKey`, the next page reads it, covers instantly and unveils — no
1463
+ flash between documents. Reduced motion snaps both halves instantly to a
1464
+ solid sheet, so navigation still never flashes the swap; without a 2D
1465
+ context (SSR, jsdom) both promises resolve immediately and nothing paints.
1466
+ `destroy()` removes the canvas, its listeners and the link interception.
1467
+
1468
+ ---
1469
+
1239
1470
  ### Logos & SVG
1240
1471
 
1241
1472
  Vector reveals for brand marks.
@@ -1408,7 +1639,7 @@ curl http://localhost:4321/api/prompts/marquee # one prompt, text/plain
1408
1639
  ```
1409
1640
 
1410
1641
  On the page, every labelled section carries a **copy prompt** chip, and the
1411
- floating **⧉ prompts (29)** button at the bottom right opens the full
1642
+ floating **⧉ prompts (38)** button at the bottom right opens the full
1412
1643
  catalogue grouped by [effect category](#effect-categories) — one click copies
1413
1644
  an effect's prompt (the prompt states its category), *copy all* puts the
1414
1645
  entire set on the clipboard. The catalogue lives in `scripts/prompts.mjs`:
@@ -1419,17 +1650,19 @@ and slotting the effect into a subcategory.
1419
1650
  **Unit smoke** (`scripts/smoke.mjs`) runs the built bundle in **jsdom** and
1420
1651
  asserts:
1421
1652
 
1422
- 1. all 47 exports are present;
1653
+ 1. all 48 exports are present;
1423
1654
  2. plugins (`ScrollTrigger`, `SplitText`, `Draggable`, `CustomEase`,
1424
1655
  `Flip`, `ScrollSmoother`) and the 4 custom eases are registered;
1425
1656
  3. every effect no-ops safely on missing targets;
1426
- 4. 24 effects mount on real markup and unmount cleanly;
1427
- 5. `preloader` ticks in both the positional and options-object call forms;
1428
- 6. `lineReveal` actually splits into masked lines (and per-character masks
1657
+ 4. 27 effects mount on real markup and unmount cleanly;
1658
+ 5. pinning effects claim `refreshPriority: 1`, so ScrollTrigger refresh
1659
+ measures in document order (root README bug class E);
1660
+ 6. `preloader` ticks in both the positional and options-object call forms;
1661
+ 7. `lineReveal` actually splits into masked lines (and per-character masks
1429
1662
  with `split: "chars"`) and restores markup on destroy;
1430
- 7. `scrambleText` restores its text, `rollText` wraps/unwraps its rows, and
1663
+ 8. `scrambleText` restores its text, `rollText` wraps/unwraps its rows, and
1431
1664
  `counter({ progress: true })` renders a scrubbed readout;
1432
- 8. `utils`, `compose` and `guard` behave per contract.
1665
+ 9. `utils`, `compose` and `guard` behave per contract.
1433
1666
 
1434
1667
  **Demo smoke** (`scripts/demo-smoke.mjs`) loads the real `demo/index.html` and
1435
1668
  executes the real `demo/demo.js` wiring against it, then asserts the effects
@@ -1462,9 +1695,9 @@ anim-kit/
1462
1695
  │ │ ├─ guard.ts reduced-motion gate
1463
1696
  │ │ ├─ util.ts toArray/one/onReady/compose/raf
1464
1697
  │ │ └─ types.ts TargetLike / Destroy / CommonOptions
1465
- │ ├─ effects/ one file per effect (24 files, 28 effect functions)
1698
+ │ ├─ effects/ one file per effect (27 files, 31 effect functions)
1466
1699
  │ ├─ styles/anim-kit.css companion stylesheet
1467
- │ └─ index.ts barrel — 47 exports
1700
+ │ └─ index.ts barrel — 48 exports
1468
1701
  ├─ demo/ visual demo (import map, no bundler)
1469
1702
  ├─ scripts/
1470
1703
  │ ├─ serve.mjs static server + /api/prompts (:4321)