@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
|
@@ -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
|
+
}
|