@cosmictraveler002/anim-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1033 -0
  3. package/dist/anim-kit.standalone.js +117 -0
  4. package/dist/anim-kit.standalone.js.map +1 -0
  5. package/dist/core/gsap.d.ts +44 -0
  6. package/dist/core/gsap.d.ts.map +1 -0
  7. package/dist/core/gsap.js +71 -0
  8. package/dist/core/gsap.js.map +1 -0
  9. package/dist/core/guard.d.ts +3 -0
  10. package/dist/core/guard.d.ts.map +1 -0
  11. package/dist/core/guard.js +18 -0
  12. package/dist/core/guard.js.map +1 -0
  13. package/dist/core/smooth-scroll.d.ts +45 -0
  14. package/dist/core/smooth-scroll.d.ts.map +1 -0
  15. package/dist/core/smooth-scroll.js +98 -0
  16. package/dist/core/smooth-scroll.js.map +1 -0
  17. package/dist/core/split.d.ts +24 -0
  18. package/dist/core/split.d.ts.map +1 -0
  19. package/dist/core/split.js +117 -0
  20. package/dist/core/split.js.map +1 -0
  21. package/dist/core/types.d.ts +21 -0
  22. package/dist/core/types.d.ts.map +1 -0
  23. package/dist/core/types.js +9 -0
  24. package/dist/core/types.js.map +1 -0
  25. package/dist/core/util.d.ts +19 -0
  26. package/dist/core/util.d.ts.map +1 -0
  27. package/dist/core/util.js +73 -0
  28. package/dist/core/util.js.map +1 -0
  29. package/dist/effects/counter.d.ts +52 -0
  30. package/dist/effects/counter.d.ts.map +1 -0
  31. package/dist/effects/counter.js +106 -0
  32. package/dist/effects/counter.js.map +1 -0
  33. package/dist/effects/cursor-follower.d.ts +19 -0
  34. package/dist/effects/cursor-follower.d.ts.map +1 -0
  35. package/dist/effects/cursor-follower.js +76 -0
  36. package/dist/effects/cursor-follower.js.map +1 -0
  37. package/dist/effects/drag-strip.d.ts +17 -0
  38. package/dist/effects/drag-strip.d.ts.map +1 -0
  39. package/dist/effects/drag-strip.js +186 -0
  40. package/dist/effects/drag-strip.js.map +1 -0
  41. package/dist/effects/hero-shrink.d.ts +15 -0
  42. package/dist/effects/hero-shrink.d.ts.map +1 -0
  43. package/dist/effects/hero-shrink.js +40 -0
  44. package/dist/effects/hero-shrink.js.map +1 -0
  45. package/dist/effects/horizontal-scroll.d.ts +17 -0
  46. package/dist/effects/horizontal-scroll.d.ts.map +1 -0
  47. package/dist/effects/horizontal-scroll.js +87 -0
  48. package/dist/effects/horizontal-scroll.js.map +1 -0
  49. package/dist/effects/line-reveal.d.ts +34 -0
  50. package/dist/effects/line-reveal.d.ts.map +1 -0
  51. package/dist/effects/line-reveal.js +71 -0
  52. package/dist/effects/line-reveal.js.map +1 -0
  53. package/dist/effects/liquid-button.d.ts +19 -0
  54. package/dist/effects/liquid-button.d.ts.map +1 -0
  55. package/dist/effects/liquid-button.js +52 -0
  56. package/dist/effects/liquid-button.js.map +1 -0
  57. package/dist/effects/logo-reveal.d.ts +15 -0
  58. package/dist/effects/logo-reveal.d.ts.map +1 -0
  59. package/dist/effects/logo-reveal.js +49 -0
  60. package/dist/effects/logo-reveal.js.map +1 -0
  61. package/dist/effects/marquee.d.ts +13 -0
  62. package/dist/effects/marquee.d.ts.map +1 -0
  63. package/dist/effects/marquee.js +143 -0
  64. package/dist/effects/marquee.js.map +1 -0
  65. package/dist/effects/mask-reveal.d.ts +36 -0
  66. package/dist/effects/mask-reveal.d.ts.map +1 -0
  67. package/dist/effects/mask-reveal.js +102 -0
  68. package/dist/effects/mask-reveal.js.map +1 -0
  69. package/dist/effects/menu-overlay.d.ts +32 -0
  70. package/dist/effects/menu-overlay.d.ts.map +1 -0
  71. package/dist/effects/menu-overlay.js +150 -0
  72. package/dist/effects/menu-overlay.js.map +1 -0
  73. package/dist/effects/nav-hide.d.ts +13 -0
  74. package/dist/effects/nav-hide.d.ts.map +1 -0
  75. package/dist/effects/nav-hide.js +68 -0
  76. package/dist/effects/nav-hide.js.map +1 -0
  77. package/dist/effects/parallax.d.ts +11 -0
  78. package/dist/effects/parallax.d.ts.map +1 -0
  79. package/dist/effects/parallax.js +48 -0
  80. package/dist/effects/parallax.js.map +1 -0
  81. package/dist/effects/preloader.d.ts +33 -0
  82. package/dist/effects/preloader.d.ts.map +1 -0
  83. package/dist/effects/preloader.js +128 -0
  84. package/dist/effects/preloader.js.map +1 -0
  85. package/dist/effects/scatter-text.d.ts +20 -0
  86. package/dist/effects/scatter-text.d.ts.map +1 -0
  87. package/dist/effects/scatter-text.js +87 -0
  88. package/dist/effects/scatter-text.js.map +1 -0
  89. package/dist/effects/stacked-cards.d.ts +24 -0
  90. package/dist/effects/stacked-cards.d.ts.map +1 -0
  91. package/dist/effects/stacked-cards.js +106 -0
  92. package/dist/effects/stacked-cards.js.map +1 -0
  93. package/dist/effects/theme-reveal.d.ts +35 -0
  94. package/dist/effects/theme-reveal.d.ts.map +1 -0
  95. package/dist/effects/theme-reveal.js +93 -0
  96. package/dist/effects/theme-reveal.js.map +1 -0
  97. package/dist/index.d.ts +77 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +61 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/styles/anim-kit.css +361 -0
  102. package/package.json +80 -0
  103. package/src/core/gsap.ts +79 -0
  104. package/src/core/guard.ts +18 -0
  105. package/src/core/smooth-scroll.ts +143 -0
  106. package/src/core/split.ts +145 -0
  107. package/src/core/types.ts +30 -0
  108. package/src/core/util.ts +79 -0
  109. package/src/effects/counter.ts +192 -0
  110. package/src/effects/cursor-follower.ts +104 -0
  111. package/src/effects/drag-strip.ts +228 -0
  112. package/src/effects/hero-shrink.ts +69 -0
  113. package/src/effects/horizontal-scroll.ts +123 -0
  114. package/src/effects/line-reveal.ts +109 -0
  115. package/src/effects/liquid-button.ts +75 -0
  116. package/src/effects/logo-reveal.ts +76 -0
  117. package/src/effects/marquee.ts +157 -0
  118. package/src/effects/mask-reveal.ts +148 -0
  119. package/src/effects/menu-overlay.ts +218 -0
  120. package/src/effects/nav-hide.ts +90 -0
  121. package/src/effects/parallax.ts +68 -0
  122. package/src/effects/preloader.ts +187 -0
  123. package/src/effects/scatter-text.ts +129 -0
  124. package/src/effects/stacked-cards.ts +154 -0
  125. package/src/effects/theme-reveal.ts +144 -0
  126. package/src/index.ts +98 -0
  127. package/src/styles/anim-kit.css +361 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Drag strip — the grabbable, infinitely looping image carousel.
3
+ *
4
+ * Built on GSAP Draggable. Why it is shaped this way (audit notes):
5
+ *
6
+ * - **Single writer.** Draggable owns the track's X transform; the loop is a
7
+ * `liveSnap` function Draggable itself calls on every move (and every frame
8
+ * of an inertia throw). Nothing else writes X — the first version let a
9
+ * 0.5s `quickTo` fight Draggable's own transform, which stuttered between
10
+ * wrapped and unwrapped positions. (Note: Draggable has no `modifiers`
11
+ * option; `liveSnap` is the supported hook.)
12
+ * - **Seamless loop.** Folding positions is only invisible when the content
13
+ * tiles: with `clone: true` (default) the strip duplicates itself until one
14
+ * tile is at least as wide as the viewport, and the fold distance is always
15
+ * a whole multiple of the tile width — the same trick `marquee()` uses.
16
+ * Without cloning there is nothing to fold against, so the strip clamps at
17
+ * the content edges instead (finite, but never a gap).
18
+ * - **Rotation on pull.** Children tilt against pointer *speed* (normalised to
19
+ * a 60fps frame so mouse and touch event rates feel identical) and spring
20
+ * back to 0 on release. Pointer movement is used rather than Draggable's
21
+ * `deltaX`, because deltaX jumps by a full loop at the seam.
22
+ * - **Fidelity.** `destroy()` kills the Draggable and both rotation tweens,
23
+ * removes cloned tiles (original markup back) and restores the inline
24
+ * cursor/user-select/touch-action the effect had overwritten.
25
+ *
26
+ * dragStrip(".ak-drag-track", { maxRotation: 60 })
27
+ *
28
+ * CSS: viewport `overflow: hidden`; track `display: flex; width: max-content;
29
+ * gap: 1rem; cursor: grab; user-select: none; touch-action: pan-y`.
30
+ */
31
+ import { gsap, Draggable, initGSAP } from "../core/gsap.js";
32
+ import { guard } from "../core/guard.js";
33
+ import { one, toArray } from "../core/util.js";
34
+ import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
35
+
36
+ export interface DragStripOptions extends CommonOptions {
37
+ /** Max rotation at full drag speed, degrees. @default 100 */
38
+ maxRotation?: number;
39
+ /** Rotation response — divisor on the normalised drag speed. @default 100 */
40
+ rotationScale?: number;
41
+ /** Spring-back duration on release, seconds. @default 1 */
42
+ settleDuration?: number;
43
+ /** Enable inertia after release (requires GSAP InertiaPlugin). @default false */
44
+ inertia?: boolean;
45
+ /** Duplicate content so the loop is seamless; `false` clamps instead. @default true */
46
+ clone?: boolean;
47
+ /** Items inside the strip that rotate. @default ":scope > *" */
48
+ item?: string;
49
+ }
50
+
51
+ export function dragStrip(target: TargetLike, options: DragStripOptions = {}): Destroy {
52
+ initGSAP();
53
+
54
+ const track = one<HTMLElement>(target);
55
+ if (!track) return () => {};
56
+
57
+ const {
58
+ maxRotation = 100,
59
+ rotationScale = 100,
60
+ settleDuration = 1,
61
+ inertia = false,
62
+ clone = true,
63
+ item = ":scope > *",
64
+ } = options;
65
+
66
+ return guard(options, () => {
67
+ const viewport = track.parentElement;
68
+ const prevStyle = {
69
+ cursor: track.style.cursor,
70
+ userSelect: track.style.userSelect,
71
+ touchAction: track.style.touchAction,
72
+ };
73
+
74
+ /* ---------------- tiling (seamless infinite) ---------------- */
75
+
76
+ const originalCount = track.children.length;
77
+ const originalNodes = Array.from(track.children);
78
+ const trackWidth = () => Math.max(track.scrollWidth, track.clientWidth);
79
+
80
+ let tile = originalCount > 0 ? trackWidth() : 0; // width of one copy
81
+ let viewportW = 1; // width of the scroll viewport (the parent)
82
+ let loop = 0; // fold distance — whole tiles, or 0 when nothing to fold
83
+ let pointerDown = false;
84
+ let throwing = false;
85
+
86
+ /** Clone a tile, stripping ids so duplicated markup stays valid. */
87
+ const cloneTile = () =>
88
+ originalNodes.map((node) => {
89
+ const copy = node.cloneNode(true);
90
+ if (copy instanceof Element) {
91
+ if (copy.id) copy.removeAttribute("id");
92
+ copy.querySelectorAll("[id]").forEach((el) => el.removeAttribute("id"));
93
+ }
94
+ return copy;
95
+ });
96
+
97
+ const measure = () => {
98
+ viewportW = Math.max(1, viewport?.clientWidth || track.clientWidth || 1);
99
+ if (tile > 0 && clone) {
100
+ // One tile must be ≥ the viewport, with a spare tile to fold into.
101
+ for (let i = 8; i > 0 && trackWidth() - tile < viewportW; i--) {
102
+ track.append(...cloneTile());
103
+ }
104
+ }
105
+ if (tile <= 0) {
106
+ loop = 0; // unmeasurable (hidden / jsdom) — drag without folding
107
+ } else if (clone) {
108
+ // Largest multiple of `tile` that still leaves room for the viewport.
109
+ loop = Math.max(0, Math.floor((trackWidth() - viewportW) / tile)) * tile;
110
+ } else {
111
+ // Finite mode: keep the content aligned, never an empty gap.
112
+ loop = Math.max(0, trackWidth() - viewportW);
113
+ }
114
+ };
115
+
116
+ const fold = (value: number) => {
117
+ if (loop <= 0) return value;
118
+ return clone
119
+ ? gsap.utils.wrap(-loop, 0, value)
120
+ : Math.max(-loop, Math.min(0, value));
121
+ };
122
+
123
+ /**
124
+ * Handed to Draggable as `liveSnap.x`. Folds while Draggable is driving
125
+ * the value (pointer down, or a throw in progress). InertiaPlugin parses
126
+ * its *end target* with both flags false — that value stays raw so the
127
+ * throw keeps its direction; syncXY folds every frame of the throw instead.
128
+ */
129
+ const foldWhenDriven = (value: number) =>
130
+ pointerDown || throwing ? fold(value) : value;
131
+
132
+ measure();
133
+
134
+ /* ---------------- rotation on pull ---------------- */
135
+
136
+ const items = toArray<HTMLElement>(item, track); // after cloning: tiles tilt too
137
+ gsap.set(items, { transformOrigin: "50% 100%" });
138
+
139
+ const followRotation = gsap.quickTo(items, "rotation", {
140
+ duration: 0.12,
141
+ ease: "power2.out",
142
+ });
143
+ const settleRotation = gsap.quickTo(items, "rotation", {
144
+ duration: settleDuration,
145
+ ease: "power3.out",
146
+ });
147
+
148
+ let lastPointerX = 0;
149
+ let lastMoveTime = 0;
150
+
151
+ /* ---------------- drag ---------------- */
152
+
153
+ track.style.cursor = "grab";
154
+ track.style.userSelect = "none";
155
+ track.style.touchAction = "pan-y";
156
+
157
+ const draggable = Draggable.create(track, {
158
+ type: "x",
159
+ dragClickables: true,
160
+ inertia,
161
+ liveSnap: { x: foldWhenDriven },
162
+ onPress() {
163
+ pointerDown = true;
164
+ throwing = false;
165
+ // A spring from the previous release has had its chance — start fresh.
166
+ if (settleRotation.tween.isActive()) settleRotation.tween.totalProgress(1);
167
+ lastPointerX = this.pointerX || 0;
168
+ lastMoveTime = performance.now();
169
+ track.style.cursor = "grabbing";
170
+ },
171
+ onDrag() {
172
+ const now = performance.now();
173
+ const dt = Math.min(64, Math.max(8, now - lastMoveTime));
174
+ const pointerX = this.pointerX || 0;
175
+ const dx = pointerX - lastPointerX;
176
+ lastPointerX = pointerX;
177
+ lastMoveTime = now;
178
+ // Pointer speed normalised to a 60fps frame → same feel for every
179
+ // event rate (mouse vs touch), unlike a raw per-event delta.
180
+ const speed = (dx / dt) * 16.667;
181
+ const rotation = gsap.utils.clamp(
182
+ -maxRotation,
183
+ maxRotation,
184
+ -(speed / rotationScale) * 100,
185
+ );
186
+ followRotation(rotation);
187
+ },
188
+ onRelease() {
189
+ pointerDown = false;
190
+ track.style.cursor = prevStyle.cursor || "grab";
191
+ // Hold the last tilt, then spring it back over settleDuration.
192
+ if (followRotation.tween.isActive()) followRotation.tween.totalProgress(1);
193
+ settleRotation(0);
194
+ },
195
+ onThrowUpdate: () => {
196
+ throwing = true; // from here on, liveSnap folds each frame
197
+ },
198
+ onThrowComplete: () => {
199
+ throwing = false;
200
+ },
201
+ // (throw interruption resets `throwing` on the next press)
202
+ })[0];
203
+
204
+ /* ---------------- resize ---------------- */
205
+
206
+ const onResize = () => {
207
+ const x = parseFloat(String(gsap.getProperty(track, "x") ?? 0)) || 0;
208
+ measure(); // widen if the viewport grew; never removes tiles
209
+ gsap.set(track, { x: fold(x) }); // re-fold into the new range
210
+ };
211
+ window.addEventListener("resize", onResize);
212
+
213
+ return () => {
214
+ window.removeEventListener("resize", onResize);
215
+ followRotation.tween.kill();
216
+ settleRotation.tween.kill();
217
+ draggable.kill();
218
+ // Restore the exact inline styles we overwrote (not clearProps:"all",
219
+ // which would also wipe the consumer's own inline styles).
220
+ track.style.cursor = prevStyle.cursor;
221
+ track.style.userSelect = prevStyle.userSelect;
222
+ track.style.touchAction = prevStyle.touchAction;
223
+ // Original markup back: drop cloned tiles, then clear our transforms.
224
+ while (track.children.length > originalCount) track.removeChild(track.lastElementChild!);
225
+ gsap.set([track, ...track.children], { clearProps: "transform,transformOrigin" });
226
+ };
227
+ });
228
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Hero shrink — the showreel video that scales down as you scroll away.
3
+ *
4
+ * fromTo(el, { scale: 1, x: 0, y: 0 },
5
+ * { x: 0, y: "49vh", scale: 0.23, force3D: true,
6
+ * scrollTrigger: { trigger: el, start: "top top",
7
+ * end: "bottom top", scrub: 1 } })
8
+ *
9
+ * The video stays full-bleed at the top of the page and shrinks toward the
10
+ * bottom-centre as the section scrolls past, with `scrub: 1` giving it that
11
+ * heavy, lagging feel.
12
+ */
13
+ import { gsap, ScrollTrigger, initGSAP } from "../core/gsap.js";
14
+ import { guard } from "../core/guard.js";
15
+ import { one } from "../core/util.js";
16
+ import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
17
+
18
+ export interface HeroShrinkOptions extends CommonOptions {
19
+ /** How far down the element drifts, as a CSS length. @default "49vh" */
20
+ offsetY?: string;
21
+ /** Final scale. @default 0.23 */
22
+ scale?: number;
23
+ /** Scrub smoothing — higher = laggier. @default 1 */
24
+ scrub?: number;
25
+ start?: string;
26
+ end?: string;
27
+ /** Shift on X at the end. @default 0 */
28
+ offsetX?: string;
29
+ }
30
+
31
+ export function heroShrink(target: TargetLike, options: HeroShrinkOptions = {}): Destroy {
32
+ initGSAP();
33
+
34
+ const el = one<HTMLElement>(target);
35
+ if (!el) return () => {};
36
+
37
+ const {
38
+ offsetY = "49vh",
39
+ offsetX = "0px",
40
+ scale = 0.23,
41
+ scrub = 1,
42
+ start = "top top",
43
+ end = "bottom top",
44
+ } = options;
45
+
46
+ return guard(options, () => {
47
+ const tween = gsap.fromTo(
48
+ el,
49
+ { scale: 1, x: 0, y: 0 },
50
+ {
51
+ x: offsetX,
52
+ y: offsetY,
53
+ scale,
54
+ force3D: true,
55
+ ease: "none",
56
+ scrollTrigger: { trigger: el, start, end, scrub },
57
+ },
58
+ );
59
+
60
+ ScrollTrigger.refresh();
61
+
62
+ return () => {
63
+ tween.scrollTrigger?.kill();
64
+ tween.kill();
65
+ gsap.set(el, { clearProps: "transform" });
66
+ ScrollTrigger.refresh();
67
+ };
68
+ });
69
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pinned horizontal scroll — the "our work" gallery.
3
+ *
4
+ * A tall section is pinned while an inner track slides left; images inside
5
+ * each panel get a secondary parallax driven by `containerAnimation`, so they
6
+ * settle as they cross the viewport *horizontally* rather than vertically.
7
+ *
8
+ * horizontalScroll(section, track, { panelImage: "img" })
9
+ *
10
+ * DOM contract:
11
+ * <section> ← pinned (trigger)
12
+ * <div class="track">← translated on X (track)
13
+ * <div class="panel">…<img>…</div> × N
14
+ */
15
+ import { gsap, ScrollTrigger, initGSAP } from "../core/gsap.js";
16
+ import { guard } from "../core/guard.js";
17
+ import { one } from "../core/util.js";
18
+ import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
19
+
20
+ export interface HorizontalScrollOptions extends CommonOptions {
21
+ /** Pinned section (trigger). Defaults to the first argument's parent. */
22
+ section?: TargetLike;
23
+ /** Selector for images inside panels that get extra parallax. */
24
+ panelImage?: string | null;
25
+ /** Scrub smoothing for the horizontal movement. @default 0.5 */
26
+ scrub?: number;
27
+ /** Scrub smoothing for the image parallax. @default 0.2 */
28
+ imageScrub?: number;
29
+ /** Final X for the track; defaults to `track.scrollWidth - innerWidth`. */
30
+ travel?: () => number;
31
+ /** Kill ScrollTriggers created for the images. @default true */
32
+ imageParallax?: boolean;
33
+ }
34
+
35
+ export function horizontalScroll(
36
+ target: TargetLike,
37
+ options: HorizontalScrollOptions = {},
38
+ ): Destroy {
39
+ initGSAP();
40
+
41
+ const track = one<HTMLElement>(target);
42
+ if (!track) return () => {};
43
+
44
+ const section =
45
+ (options.section ? one<HTMLElement>(options.section) : null) ??
46
+ (track.closest("section") as HTMLElement | null) ??
47
+ track;
48
+
49
+ const {
50
+ scrub = 0.5,
51
+ imageScrub = 0.2,
52
+ panelImage = "img",
53
+ imageParallax = true,
54
+ } = options;
55
+
56
+ return guard(options, () => {
57
+ const travel = () =>
58
+ options.travel
59
+ ? options.travel()
60
+ : Math.max(0, track.scrollWidth - window.innerWidth);
61
+
62
+ const tweens: gsap.core.Tween[] = [];
63
+ const triggers: ScrollTrigger[] = [];
64
+
65
+ const main = gsap.to(track, {
66
+ x: () => -travel(),
67
+ ease: "none",
68
+ scrollTrigger: {
69
+ trigger: section,
70
+ pin: true,
71
+ scrub,
72
+ end: () => "+=" + travel(),
73
+ invalidateOnRefresh: true,
74
+ anticipatePin: 1,
75
+ },
76
+ });
77
+ tweens.push(main);
78
+ if (main.scrollTrigger) triggers.push(main.scrollTrigger);
79
+
80
+ if (imageParallax && panelImage) {
81
+ const images = gsap.utils.toArray<HTMLElement>(panelImage, track);
82
+ images.forEach((img) => {
83
+ const t = gsap.fromTo(
84
+ img,
85
+ { scale: 0.9, yPercent: 30 },
86
+ {
87
+ scale: 1,
88
+ yPercent: 0,
89
+ ease: "power4.out",
90
+ scrollTrigger: {
91
+ trigger: img,
92
+ containerAnimation: main,
93
+ start: "left 100%",
94
+ end: "left 20%",
95
+ scrub: imageScrub,
96
+ },
97
+ },
98
+ );
99
+ tweens.push(t);
100
+ if (t.scrollTrigger) triggers.push(t.scrollTrigger);
101
+ });
102
+ }
103
+
104
+ // Late-loading images change track width — refresh once they land.
105
+ const onLoad = () => ScrollTrigger.refresh();
106
+ const imgs = Array.from(track.querySelectorAll("img"));
107
+ imgs.forEach((img) => {
108
+ if (!img.complete) img.addEventListener("load", onLoad, { once: true });
109
+ });
110
+ window.addEventListener("load", onLoad);
111
+
112
+ ScrollTrigger.refresh();
113
+
114
+ return () => {
115
+ window.removeEventListener("load", onLoad);
116
+ imgs.forEach((img) => img.removeEventListener("load", onLoad));
117
+ triggers.forEach((t) => t.kill());
118
+ tweens.forEach((t) => t.kill());
119
+ gsap.set(track, { clearProps: "transform" });
120
+ ScrollTrigger.refresh();
121
+ };
122
+ });
123
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Line reveal — the masked, staggered text reveal used all over DZ!NR.
3
+ *
4
+ * Text is split into lines, each line gets an `overflow:hidden` mask, and the
5
+ * inner line slides up from `y:100%` to `y:0%`.
6
+ *
7
+ * Two modes:
8
+ * mode: "scroll" → plays when the element enters the viewport and reverses
9
+ * when it leaves (`toggleActions: "play reverse play reverse"`,
10
+ * start `top 90%` / end `bottom 10%`) — their default.
11
+ * mode: "immediate"→ plays right away (used above the fold with
12
+ * `animateOnScroll: false`).
13
+ */
14
+ import { gsap, ScrollTrigger, initGSAP, EASES } from "../core/gsap.js";
15
+ import { guard } from "../core/guard.js";
16
+ import { split, type SplitResult } from "../core/split.js";
17
+ import { toArray } from "../core/util.js";
18
+ import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
19
+
20
+ export interface LineRevealOptions extends CommonOptions {
21
+ /** "scroll" plays on enter/reverse on leave; "immediate" plays at once. */
22
+ mode?: "scroll" | "immediate";
23
+ /** Stagger between lines, seconds. @default 0.1 */
24
+ stagger?: number;
25
+ /** Animation duration, seconds. @default 1 */
26
+ duration?: number;
27
+ /** GSAP ease name. @default "power4.out" */
28
+ ease?: string;
29
+ /** Delay before playing, seconds. @default 0 */
30
+ delay?: number;
31
+ /** ScrollTrigger start position. @default "top 90%" */
32
+ start?: string;
33
+ /** ScrollTrigger end position. @default "bottom 10%" */
34
+ end?: string;
35
+ }
36
+
37
+ export function lineReveal(target: TargetLike, options: LineRevealOptions = {}): Destroy {
38
+ initGSAP();
39
+
40
+ const {
41
+ mode = "scroll",
42
+ stagger = 0.1,
43
+ duration = 1,
44
+ ease = "power4.out",
45
+ delay = 0,
46
+ start = "top 90%",
47
+ end = "bottom 10%",
48
+ } = options;
49
+
50
+ const els = toArray<HTMLElement>(target);
51
+ if (!els.length) return () => {};
52
+
53
+ return guard(options, run);
54
+
55
+ function run(): Destroy {
56
+ const splits: SplitResult[] = [];
57
+ const tweens: gsap.core.Tween[] = [];
58
+
59
+ els.forEach((el) => {
60
+ const res = split(el, {
61
+ type: "lines",
62
+ mask: true,
63
+ linesClass: "ak-line++",
64
+ lineThreshold: 0.05,
65
+ });
66
+ splits.push(res);
67
+
68
+ // SplitText copies aria-labels onto the fragments; drop them for a11y.
69
+ el.querySelectorAll("[aria-label]").forEach((n) => n.removeAttribute("aria-label"));
70
+
71
+ if (!res.elements.length) return;
72
+
73
+ gsap.set(res.elements, { y: "100%" });
74
+
75
+ const vars: gsap.TweenVars = {
76
+ y: "0%",
77
+ duration,
78
+ stagger,
79
+ ease,
80
+ delay,
81
+ overwrite: "auto",
82
+ };
83
+
84
+ if (mode === "scroll") {
85
+ vars.scrollTrigger = {
86
+ trigger: el,
87
+ start,
88
+ end,
89
+ toggleActions: "play reverse play reverse",
90
+ };
91
+ }
92
+
93
+ tweens.push(gsap.to(res.elements, vars));
94
+ });
95
+
96
+ ScrollTrigger.refresh();
97
+
98
+ return () => {
99
+ tweens.forEach((t) => {
100
+ t.scrollTrigger?.kill();
101
+ t.kill();
102
+ });
103
+ splits.forEach((s) => s.revert());
104
+ ScrollTrigger.refresh();
105
+ };
106
+ }
107
+ }
108
+
109
+ export { EASES };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Liquid button — the SVG wave that floods a button from the bottom on hover.
3
+ *
4
+ * Pure CSS/SVG geometry + a transition, exactly as the studio does it: an
5
+ * oversized (`160%`) SVG sits below the button and translates up on hover with
6
+ * a curved top edge, so the fill looks like liquid rising rather than a flat
7
+ * rectangle. Text colour cross-fades on top of it.
8
+ *
9
+ * Usage:
10
+ * <button class="ak-liquid" data-ak-liquid>
11
+ * <svg class="ak-liquid__wave" viewBox="0 0 100 100" preserveAspectRatio="none">
12
+ * <path d="M0,30 Q50,-5 100,30 L100,100 L0,100 Z"/>
13
+ * </svg>
14
+ * <span class="ak-liquid__label">Lets Talk</span>
15
+ * </button>
16
+ *
17
+ * This module just wires the custom properties (`--ak-liquid-duration`,
18
+ * `--ak-liquid-fill`) from JS options onto the markup, so a single stylesheet
19
+ * drives every instance.
20
+ */
21
+ import { toArray } from "../core/util.js";
22
+ import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
23
+
24
+ export interface LiquidButtonOptions extends CommonOptions {
25
+ /** Wave travel time in ms. @default 900 */
26
+ duration?: number;
27
+ /** CSS colour for the wave fill. @default "var(--ak-primary, #6c5ce7)" */
28
+ fill?: string;
29
+ /** Which edge the liquid rises from. @default "up" */
30
+ direction?: "up" | "down";
31
+ /** Colour the label turns on hover. @default "#fff" */
32
+ labelColor?: string;
33
+ }
34
+
35
+ export function liquidButton(
36
+ target: TargetLike,
37
+ options: LiquidButtonOptions = {},
38
+ ): Destroy {
39
+ const els = toArray<HTMLElement>(target);
40
+ if (!els.length) return () => {};
41
+
42
+ const {
43
+ duration = 900,
44
+ fill = "var(--ak-primary, #6c5ce7)",
45
+ direction = "up",
46
+ labelColor = "#fff",
47
+ } = options;
48
+
49
+ els.forEach((el) => {
50
+ el.dataset.akLiquid = direction;
51
+ el.style.setProperty("--ak-liquid-duration", `${duration}ms`);
52
+ el.style.setProperty("--ak-liquid-fill", fill);
53
+ el.style.setProperty("--ak-liquid-label", labelColor);
54
+ });
55
+
56
+ return () => {
57
+ els.forEach((el) => {
58
+ delete el.dataset.akLiquid;
59
+ el.style.removeProperty("--ak-liquid-duration");
60
+ el.style.removeProperty("--ak-liquid-fill");
61
+ el.style.removeProperty("--ak-liquid-label");
62
+ });
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Underline sweep — the `after:` pseudo-element that wipes left→right on
68
+ * hover. Applied as a class so it works with zero JS; this helper exists so
69
+ * the same behaviour can be scoped programmatically.
70
+ */
71
+ export function underlineLink(target: TargetLike): Destroy {
72
+ const els = toArray<HTMLElement>(target);
73
+ els.forEach((el) => el.classList.add("ak-underline"));
74
+ return () => els.forEach((el) => el.classList.remove("ak-underline"));
75
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Logo reveal — the DZ!NR wordmark that assembles letter by letter.
3
+ *
4
+ * Every path inside the SVG gets class `svg-anim-path`, is pre-set to
5
+ * `y:-100%, opacity:0`, then slides down into place with a 0.05s stagger when
6
+ * the mark scrolls into view (reversing back out when it leaves).
7
+ *
8
+ * logoReveal(".logo svg") ← animates [data-logo-path] children
9
+ * logoReveal(el, { path: ".my-class" })
10
+ */
11
+ import { gsap, ScrollTrigger, initGSAP } from "../core/gsap.js";
12
+ import { guard } from "../core/guard.js";
13
+ import { one, toArray } from "../core/util.js";
14
+ import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
15
+
16
+ export interface LogoRevealOptions extends CommonOptions {
17
+ /** Selector for the animatable paths inside the SVG. @default "[data-logo-path], .svg-anim-path" */
18
+ path?: string;
19
+ /** Stagger between paths, seconds. @default 0.05 */
20
+ stagger?: number;
21
+ duration?: number;
22
+ ease?: string;
23
+ start?: string;
24
+ end?: string;
25
+ /** Play once and never reverse. @default false */
26
+ once?: boolean;
27
+ }
28
+
29
+ export function logoReveal(target: TargetLike, options: LogoRevealOptions = {}): Destroy {
30
+ initGSAP();
31
+
32
+ const el = one<HTMLElement>(target);
33
+ if (!el) return () => {};
34
+
35
+ const {
36
+ path = "[data-logo-path], .svg-anim-path",
37
+ stagger = 0.05,
38
+ duration = 1,
39
+ ease = "power2.out",
40
+ start = "top 80%",
41
+ end = "bottom top",
42
+ once = false,
43
+ } = options;
44
+
45
+ return guard(options, () => {
46
+ const paths = toArray<Element>(path, el);
47
+ if (!paths.length) return () => {};
48
+
49
+ gsap.set(paths, { yPercent: -100, opacity: 0, willChange: "transform" });
50
+
51
+ const tween = gsap.to(paths, {
52
+ yPercent: 0,
53
+ opacity: 1,
54
+ duration,
55
+ delay: 0,
56
+ ease,
57
+ stagger,
58
+ scrollTrigger: {
59
+ trigger: el,
60
+ start,
61
+ end,
62
+ once,
63
+ toggleActions: once ? undefined : "play reverse play reverse",
64
+ },
65
+ });
66
+
67
+ ScrollTrigger.refresh();
68
+
69
+ return () => {
70
+ tween.scrollTrigger?.kill();
71
+ tween.kill();
72
+ gsap.set(paths, { clearProps: "all" });
73
+ ScrollTrigger.refresh();
74
+ };
75
+ });
76
+ }