@cosmictraveler002/anim-kit 1.5.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 (51) hide show
  1. package/README.md +245 -21
  2. package/dist/anim-kit.standalone.js +5 -5
  3. package/dist/anim-kit.standalone.js.map +1 -1
  4. package/dist/effects/horizontal-scroll.d.ts.map +1 -1
  5. package/dist/effects/horizontal-scroll.js +3 -0
  6. package/dist/effects/horizontal-scroll.js.map +1 -1
  7. package/dist/effects/ink-wipe.d.ts +41 -0
  8. package/dist/effects/ink-wipe.d.ts.map +1 -0
  9. package/dist/effects/ink-wipe.js +342 -0
  10. package/dist/effects/ink-wipe.js.map +1 -0
  11. package/dist/effects/scatter-text.d.ts.map +1 -1
  12. package/dist/effects/scatter-text.js +3 -0
  13. package/dist/effects/scatter-text.js.map +1 -1
  14. package/dist/effects/stacked-cards.d.ts.map +1 -1
  15. package/dist/effects/stacked-cards.js +3 -0
  16. package/dist/effects/stacked-cards.js.map +1 -1
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/three/coverflow-wheel.d.ts +26 -0
  22. package/dist/three/coverflow-wheel.d.ts.map +1 -0
  23. package/dist/three/coverflow-wheel.js +495 -0
  24. package/dist/three/coverflow-wheel.js.map +1 -0
  25. package/dist/three/dither-reveal.d.ts +36 -0
  26. package/dist/three/dither-reveal.d.ts.map +1 -0
  27. package/dist/three/dither-reveal.js +292 -0
  28. package/dist/three/dither-reveal.js.map +1 -0
  29. package/dist/three/index.d.ts +8 -0
  30. package/dist/three/index.d.ts.map +1 -1
  31. package/dist/three/index.js +4 -0
  32. package/dist/three/index.js.map +1 -1
  33. package/dist/three/tear-reveal.d.ts +54 -0
  34. package/dist/three/tear-reveal.d.ts.map +1 -0
  35. package/dist/three/tear-reveal.js +269 -0
  36. package/dist/three/tear-reveal.js.map +1 -0
  37. package/dist/three/wordmark-wave.d.ts +29 -0
  38. package/dist/three/wordmark-wave.d.ts.map +1 -0
  39. package/dist/three/wordmark-wave.js +461 -0
  40. package/dist/three/wordmark-wave.js.map +1 -0
  41. package/package.json +1 -1
  42. package/src/effects/horizontal-scroll.ts +3 -0
  43. package/src/effects/ink-wipe.ts +409 -0
  44. package/src/effects/scatter-text.ts +3 -0
  45. package/src/effects/stacked-cards.ts +3 -0
  46. package/src/index.ts +3 -0
  47. package/src/three/coverflow-wheel.ts +569 -0
  48. package/src/three/dither-reveal.ts +365 -0
  49. package/src/three/index.ts +8 -0
  50. package/src/three/tear-reveal.ts +351 -0
  51. 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.5.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.5.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.5.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.5.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.5.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.5.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.5.0/dist/index.js",
152
- "@cosmictraveler002/anim-kit/three": "https://cdn.jsdelivr.net/npm/@cosmictraveler002/anim-kit@1.5.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.5.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
@@ -329,7 +330,9 @@ prompt dock, and backs the `category` / `subcategory` fields on
329
330
  | Scroll & media | Parallax & depth | `parallax` |
330
331
  | Scroll & media | Heroes & media | `heroShrink`, `mediaSettle` |
331
332
  | Scroll & media | Enter reveals | `revealRule`, `unfoldReveal`, `clipWipe` |
332
- | WebGL | Shader media | `webglMedia`, `glRail` |
333
+ | WebGL | Shader media | `webglMedia`, `glRail`, `coverflowWheel` |
334
+ | WebGL | Scenes & overlays | `tearReveal`, `ditherReveal` |
335
+ | WebGL | Kinetic type | `wordmarkWave` |
333
336
  | Loops & marquees | Marquees | `marquee` |
334
337
  | Loops & marquees | Draggables & rails | `dragStrip`, `dragRail` |
335
338
  | Loops & marquees | Equalizers | `audioBars` |
@@ -340,6 +343,7 @@ prompt dock, and backs the `category` / `subcategory` fields on
340
343
  | Navigation & overlays | Cursors | `cursorFollower` |
341
344
  | Intros & transitions | Preloaders | `preloader` |
342
345
  | Intros & transitions | Theme wipes | `themeReveal` |
346
+ | Intros & transitions | Page transitions | `inkWipe` |
343
347
  | Logos & SVG | Path reveals | `logoReveal` |
344
348
 
345
349
  **Growing the library:** a subcategory is the slot sibling effects land in —
@@ -806,7 +810,8 @@ the entrance.
806
810
 
807
811
  ### WebGL
808
812
 
809
- 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
810
815
  category behind the `@cosmictraveler002/anim-kit/three` subpath: `three` is an
811
816
  **optional peer** (see [Subpath exports](#subpath-exports)), so the core
812
817
  barrel and the standalone bundle stay three-free.
@@ -920,6 +925,183 @@ user-select: none`.
920
925
 
921
926
  ---
922
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
+
923
1105
  ### Loops & marquees
924
1106
 
925
1107
  Continuous motion — marquees, draggable rails, equaliser bars.
@@ -1245,6 +1427,46 @@ theme.set("dark"); theme.toggle(); theme.current(); theme.destroy();
1245
1427
 
1246
1428
  ---
1247
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
+
1248
1470
  ### Logos & SVG
1249
1471
 
1250
1472
  Vector reveals for brand marks.
@@ -1417,7 +1639,7 @@ curl http://localhost:4321/api/prompts/marquee # one prompt, text/plain
1417
1639
  ```
1418
1640
 
1419
1641
  On the page, every labelled section carries a **copy prompt** chip, and the
1420
- floating **⧉ prompts (29)** button at the bottom right opens the full
1642
+ floating **⧉ prompts (38)** button at the bottom right opens the full
1421
1643
  catalogue grouped by [effect category](#effect-categories) — one click copies
1422
1644
  an effect's prompt (the prompt states its category), *copy all* puts the
1423
1645
  entire set on the clipboard. The catalogue lives in `scripts/prompts.mjs`:
@@ -1428,17 +1650,19 @@ and slotting the effect into a subcategory.
1428
1650
  **Unit smoke** (`scripts/smoke.mjs`) runs the built bundle in **jsdom** and
1429
1651
  asserts:
1430
1652
 
1431
- 1. all 47 exports are present;
1653
+ 1. all 48 exports are present;
1432
1654
  2. plugins (`ScrollTrigger`, `SplitText`, `Draggable`, `CustomEase`,
1433
1655
  `Flip`, `ScrollSmoother`) and the 4 custom eases are registered;
1434
1656
  3. every effect no-ops safely on missing targets;
1435
- 4. 24 effects mount on real markup and unmount cleanly;
1436
- 5. `preloader` ticks in both the positional and options-object call forms;
1437
- 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
1438
1662
  with `split: "chars"`) and restores markup on destroy;
1439
- 7. `scrambleText` restores its text, `rollText` wraps/unwraps its rows, and
1663
+ 8. `scrambleText` restores its text, `rollText` wraps/unwraps its rows, and
1440
1664
  `counter({ progress: true })` renders a scrubbed readout;
1441
- 8. `utils`, `compose` and `guard` behave per contract.
1665
+ 9. `utils`, `compose` and `guard` behave per contract.
1442
1666
 
1443
1667
  **Demo smoke** (`scripts/demo-smoke.mjs`) loads the real `demo/index.html` and
1444
1668
  executes the real `demo/demo.js` wiring against it, then asserts the effects
@@ -1471,9 +1695,9 @@ anim-kit/
1471
1695
  │ │ ├─ guard.ts reduced-motion gate
1472
1696
  │ │ ├─ util.ts toArray/one/onReady/compose/raf
1473
1697
  │ │ └─ types.ts TargetLike / Destroy / CommonOptions
1474
- │ ├─ effects/ one file per effect (24 files, 28 effect functions)
1698
+ │ ├─ effects/ one file per effect (27 files, 31 effect functions)
1475
1699
  │ ├─ styles/anim-kit.css companion stylesheet
1476
- │ └─ index.ts barrel — 47 exports
1700
+ │ └─ index.ts barrel — 48 exports
1477
1701
  ├─ demo/ visual demo (import map, no bundler)
1478
1702
  ├─ scripts/
1479
1703
  │ ├─ serve.mjs static server + /api/prompts (:4321)