@cosmictraveler002/anim-kit 1.1.0 → 1.2.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/README.md +114 -29
- package/dist/anim-kit.standalone.js +18 -8
- package/dist/anim-kit.standalone.js.map +1 -1
- package/dist/core/gsap.d.ts +2 -1
- package/dist/core/gsap.d.ts.map +1 -1
- package/dist/core/gsap.js +3 -2
- package/dist/core/gsap.js.map +1 -1
- package/dist/effects/clip-wipe.d.ts +10 -2
- package/dist/effects/clip-wipe.d.ts.map +1 -1
- package/dist/effects/clip-wipe.js +30 -9
- package/dist/effects/clip-wipe.js.map +1 -1
- package/dist/effects/cursor-follower.d.ts.map +1 -1
- package/dist/effects/cursor-follower.js +22 -4
- package/dist/effects/cursor-follower.js.map +1 -1
- package/dist/effects/flip-words.d.ts +31 -0
- package/dist/effects/flip-words.d.ts.map +1 -0
- package/dist/effects/flip-words.js +104 -0
- package/dist/effects/flip-words.js.map +1 -0
- package/dist/effects/magnetic.d.ts +15 -0
- package/dist/effects/magnetic.d.ts.map +1 -0
- package/dist/effects/magnetic.js +59 -0
- package/dist/effects/magnetic.js.map +1 -0
- package/dist/effects/media-settle.d.ts +2 -0
- package/dist/effects/media-settle.d.ts.map +1 -1
- package/dist/effects/media-settle.js +5 -2
- package/dist/effects/media-settle.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/gsap.ts +3 -2
- package/src/effects/clip-wipe.ts +50 -10
- package/src/effects/cursor-follower.ts +21 -4
- package/src/effects/flip-words.ts +148 -0
- package/src/effects/magnetic.ts +87 -0
- package/src/effects/media-settle.ts +7 -1
- package/src/index.ts +9 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,oBAAoB;AACpB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAExH,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3F,8BAA8B;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGlE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGlD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAGlE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAG9E,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGxD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGxD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGhD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAGtD,qBAAqB;AACrB,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAG/C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGlD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGpD,2BAA2B;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGpD,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGzE,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAG1D,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGxD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGxD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmictraveler002/anim-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Modular, framework-agnostic animation library — GSAP + ScrollTrigger + Lenis effects packaged as composable ES modules.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/core/gsap.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { SplitText } from "gsap/SplitText";
|
|
|
10
10
|
import { Draggable } from "gsap/Draggable";
|
|
11
11
|
import { CustomEase } from "gsap/CustomEase";
|
|
12
12
|
import { ScrollSmoother } from "gsap/ScrollSmoother";
|
|
13
|
+
import { Flip } from "gsap/Flip";
|
|
13
14
|
import { toArray } from "./util.js";
|
|
14
15
|
import type { TargetLike } from "./types.js";
|
|
15
16
|
|
|
16
|
-
export { gsap, ScrollTrigger, SplitText, Draggable, CustomEase, ScrollSmoother };
|
|
17
|
+
export { gsap, ScrollTrigger, SplitText, Draggable, CustomEase, ScrollSmoother, Flip };
|
|
17
18
|
|
|
18
19
|
/** Named eases recovered from dzinrstudio.com's bundles. */
|
|
19
20
|
export const EASES = {
|
|
@@ -34,7 +35,7 @@ export function initGSAP(): void {
|
|
|
34
35
|
if (initted || typeof window === "undefined") return;
|
|
35
36
|
initted = true;
|
|
36
37
|
|
|
37
|
-
gsap.registerPlugin(ScrollTrigger, SplitText, Draggable, CustomEase);
|
|
38
|
+
gsap.registerPlugin(ScrollTrigger, SplitText, Draggable, CustomEase, Flip);
|
|
38
39
|
|
|
39
40
|
// ScrollSmoother is a bonus for people who prefer it over Lenis; it is only
|
|
40
41
|
// registered so `ScrollSmoother.create()` works if you call it yourself.
|
package/src/effects/clip-wipe.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Clip wipe — a `clip-path: inset()` reveal.
|
|
3
3
|
*
|
|
4
|
-
* The element is collapsed behind one edge (or inside a frame margin)
|
|
5
|
-
* the inset animates to zero, wiping it into view:
|
|
4
|
+
* The element is collapsed behind one edge, corner (or inside a frame margin)
|
|
5
|
+
* and the inset animates to zero, wiping it into view:
|
|
6
6
|
*
|
|
7
|
-
* from: "left"
|
|
8
|
-
* from: "right"
|
|
9
|
-
* from: "top"
|
|
10
|
-
* from: "bottom"
|
|
11
|
-
* from: "
|
|
7
|
+
* from: "left" inset(0 100% 0 0) → grows rightward from the left edge
|
|
8
|
+
* from: "right" inset(0 0 0 100%) → grows leftward from the right edge
|
|
9
|
+
* from: "top" inset(0 0 100% 0) → grows downward from the top edge
|
|
10
|
+
* from: "bottom" inset(100% 0 0 0) → grows upward from the bottom edge
|
|
11
|
+
* from: "top-left" inset(0 100% 100% 0) → opens toward the bottom-right corner
|
|
12
|
+
* from: "top-right" inset(0 0 100% 100%) → opens toward the bottom-left corner
|
|
13
|
+
* from: "bottom-left" inset(100% 100% 0 0) → opens toward the top-right corner
|
|
14
|
+
* from: "bottom-right" inset(100% 0 0 100%) → opens toward the top-left corner
|
|
15
|
+
* from: "frame" inset(15% 15% 15% 15%) → opens out of a centered frame
|
|
16
|
+
*
|
|
17
|
+
* The wipe can also be bound to scroll progress (`scrub`), so the inset
|
|
18
|
+
* opens as the element travels through the viewport — the reveal then
|
|
19
|
+
* reverses with the scroll instead of playing once.
|
|
12
20
|
*
|
|
13
21
|
* Works on images, videos, blocks and text. destroy() kills the tween and
|
|
14
22
|
* removes the inline clip-path.
|
|
@@ -18,9 +26,20 @@ import { guard } from "../core/guard.js";
|
|
|
18
26
|
import { toArray } from "../core/util.js";
|
|
19
27
|
import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
|
|
20
28
|
|
|
29
|
+
export type ClipWipeFrom =
|
|
30
|
+
| "left"
|
|
31
|
+
| "right"
|
|
32
|
+
| "top"
|
|
33
|
+
| "bottom"
|
|
34
|
+
| "top-left"
|
|
35
|
+
| "top-right"
|
|
36
|
+
| "bottom-left"
|
|
37
|
+
| "bottom-right"
|
|
38
|
+
| "frame";
|
|
39
|
+
|
|
21
40
|
export interface ClipWipeOptions extends CommonOptions {
|
|
22
|
-
/** Which edge the wipe starts from. @default "left" */
|
|
23
|
-
from?:
|
|
41
|
+
/** Which edge or corner the wipe starts from. @default "left" */
|
|
42
|
+
from?: ClipWipeFrom;
|
|
24
43
|
/** Frame margin in % (only used when `from: "frame"`). @default 15 */
|
|
25
44
|
inset?: number;
|
|
26
45
|
/** Animation duration, seconds. @default 1 */
|
|
@@ -35,8 +54,15 @@ export interface ClipWipeOptions extends CommonOptions {
|
|
|
35
54
|
mode?: "scroll" | "immediate";
|
|
36
55
|
/** ScrollTrigger start position. @default "top 85%" */
|
|
37
56
|
start?: string;
|
|
57
|
+
/** ScrollTrigger end position (scrub mode). @default "top 20%" */
|
|
58
|
+
end?: string;
|
|
38
59
|
/** Re-wipe when leaving / re-entering the viewport. @default false */
|
|
39
60
|
replay?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Bind the wipe to scroll progress instead of playing it on enter —
|
|
63
|
+
* number = scrub smoothing seconds, `true` = immediate. unset = one-shot.
|
|
64
|
+
*/
|
|
65
|
+
scrub?: number | boolean;
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
const FROM: Record<string, string> = {
|
|
@@ -44,6 +70,10 @@ const FROM: Record<string, string> = {
|
|
|
44
70
|
right: "inset(0% 0% 0% 100%)",
|
|
45
71
|
top: "inset(0% 0% 100% 0%)",
|
|
46
72
|
bottom: "inset(100% 0% 0% 0%)",
|
|
73
|
+
"top-left": "inset(0% 100% 100% 0%)",
|
|
74
|
+
"top-right": "inset(0% 0% 100% 100%)",
|
|
75
|
+
"bottom-left": "inset(100% 100% 0% 0%)",
|
|
76
|
+
"bottom-right": "inset(100% 0% 0% 100%)",
|
|
47
77
|
};
|
|
48
78
|
const TO = "inset(0% 0% 0% 0%)";
|
|
49
79
|
|
|
@@ -59,7 +89,9 @@ export function clipWipe(target: TargetLike, options: ClipWipeOptions = {}): Des
|
|
|
59
89
|
delay = 0,
|
|
60
90
|
mode = "scroll",
|
|
61
91
|
start = "top 85%",
|
|
92
|
+
end = "top 20%",
|
|
62
93
|
replay = false,
|
|
94
|
+
scrub,
|
|
63
95
|
} = options;
|
|
64
96
|
|
|
65
97
|
const els = toArray<HTMLElement>(target);
|
|
@@ -80,7 +112,15 @@ export function clipWipe(target: TargetLike, options: ClipWipeOptions = {}): Des
|
|
|
80
112
|
delay,
|
|
81
113
|
overwrite: "auto",
|
|
82
114
|
};
|
|
83
|
-
if (
|
|
115
|
+
if (scrub !== undefined) {
|
|
116
|
+
vars.ease = "none";
|
|
117
|
+
vars.scrollTrigger = {
|
|
118
|
+
trigger: els[0],
|
|
119
|
+
start,
|
|
120
|
+
end,
|
|
121
|
+
scrub: scrub === true ? true : scrub,
|
|
122
|
+
};
|
|
123
|
+
} else if (mode === "scroll") {
|
|
84
124
|
vars.scrollTrigger = {
|
|
85
125
|
trigger: els[0],
|
|
86
126
|
start,
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* whole thing fades/blurs in when the pointer enters the hot zone. The label
|
|
6
6
|
* uses `mix-blend-mode: exclusion` so it inverts over any background.
|
|
7
7
|
*
|
|
8
|
+
* The tag is pinned `position: fixed` and driven in VIEWPORT space
|
|
9
|
+
* (clientX/clientY + offset), so it can live anywhere in the DOM — it only
|
|
10
|
+
* reacts while the pointer is over the zone.
|
|
11
|
+
*
|
|
8
12
|
* cursorFollower(zone, { follower: ".ak-cursor" })
|
|
9
13
|
*/
|
|
10
14
|
import { gsap, initGSAP, killTweens } from "../core/gsap.js";
|
|
@@ -51,7 +55,11 @@ export function cursorFollower(
|
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
el.style.mixBlendMode = blendMode as never;
|
|
54
|
-
|
|
58
|
+
// Viewport space: position:fixed + clientX/clientY keeps the tag at the
|
|
59
|
+
// pointer wherever it sits in the DOM. (The old position:absolute +
|
|
60
|
+
// zone-relative math only lined up when the tag was a child of the zone —
|
|
61
|
+
// a body-level tag landed near the document origin instead, off-screen.)
|
|
62
|
+
el.style.position = "fixed";
|
|
55
63
|
el.style.left = "0";
|
|
56
64
|
el.style.top = "0";
|
|
57
65
|
el.style.pointerEvents = "none";
|
|
@@ -61,10 +69,19 @@ export function cursorFollower(
|
|
|
61
69
|
const xTo = gsap.quickTo(el, "x", { ...spring, duration: 0.4, ease: "power3" });
|
|
62
70
|
const yTo = gsap.quickTo(el, "y", { ...spring, duration: 0.4, ease: "power3" });
|
|
63
71
|
|
|
72
|
+
// First move lands on the pointer; later moves spring after it — starting
|
|
73
|
+
// from (0,0), the spring would otherwise carry the tag in from the corner.
|
|
74
|
+
let snapped = false;
|
|
64
75
|
const onMove = (e: PointerEvent) => {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
const x = e.clientX + offset;
|
|
77
|
+
const y = e.clientY + offset;
|
|
78
|
+
if (snapped) {
|
|
79
|
+
xTo(x);
|
|
80
|
+
yTo(y);
|
|
81
|
+
} else {
|
|
82
|
+
gsap.set(el, { x, y });
|
|
83
|
+
snapped = true;
|
|
84
|
+
}
|
|
68
85
|
if (fade) gsap.to(el, { opacity: 1, duration: 0.25, overwrite: "auto" });
|
|
69
86
|
};
|
|
70
87
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flip word transfer — words glide from one layout into another (FLIP).
|
|
3
|
+
*
|
|
4
|
+
* The source block holds the words; `to` is the block they land in. Both
|
|
5
|
+
* blocks should share one grid cell so the page never reflows when the
|
|
6
|
+
* words move:
|
|
7
|
+
*
|
|
8
|
+
* .flip-stage { display: grid; }
|
|
9
|
+
* .flip-stage > div { grid-area: 1 / 1; } // source + destination overlap
|
|
10
|
+
*
|
|
11
|
+
* On trigger every word is measured where it stands, moved into the
|
|
12
|
+
* destination, and animated from its old position (the classic FLIP
|
|
13
|
+
* technique), with a mid-flight squash so the words pop as they travel.
|
|
14
|
+
* Because the measurement happens up front, the words *look* like they are
|
|
15
|
+
* still in the source at scroll progress 0 even though they already live in
|
|
16
|
+
* the destination — so a scrubbed transfer reverses perfectly.
|
|
17
|
+
*
|
|
18
|
+
* flipWords("[data-flip-from]", { to: "[data-flip-to]", scrub: 1 });
|
|
19
|
+
*
|
|
20
|
+
* destroy() kills the timeline, puts every word back in its original
|
|
21
|
+
* parent (in the original order) and restores its inline transform.
|
|
22
|
+
*/
|
|
23
|
+
import { gsap, Flip, ScrollTrigger, initGSAP, killTweens } from "../core/gsap.js";
|
|
24
|
+
import { guard } from "../core/guard.js";
|
|
25
|
+
import { one, toArray } from "../core/util.js";
|
|
26
|
+
import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
|
|
27
|
+
|
|
28
|
+
export interface FlipWordsOptions extends CommonOptions {
|
|
29
|
+
/** Destination block — every word is moved into it. */
|
|
30
|
+
to: TargetLike;
|
|
31
|
+
/**
|
|
32
|
+
* The words inside the source.
|
|
33
|
+
* @default `[data-flip-word]` matches, else the source's element children
|
|
34
|
+
*/
|
|
35
|
+
words?: TargetLike;
|
|
36
|
+
/** Seconds for one word's travel. @default 1.4 */
|
|
37
|
+
duration?: number;
|
|
38
|
+
/** GSAP ease. @default "power4.inOut" */
|
|
39
|
+
ease?: string;
|
|
40
|
+
/** Seconds between word starts. @default 0.2 */
|
|
41
|
+
stagger?: number;
|
|
42
|
+
/** Mid-flight scale a word squashes to (0 disables the squash). @default 0.2 */
|
|
43
|
+
scale?: number;
|
|
44
|
+
/** "scroll" plays on enter (reverses on leave-back), "immediate" plays now. @default "scroll" */
|
|
45
|
+
mode?: "scroll" | "immediate";
|
|
46
|
+
/**
|
|
47
|
+
* Bind the transfer to scroll progress instead of playing it on enter —
|
|
48
|
+
* number = scrub smoothing seconds, `true` = immediate. unset = one-shot.
|
|
49
|
+
*/
|
|
50
|
+
scrub?: number | boolean;
|
|
51
|
+
/** ScrollTrigger start. @default "top 75%" */
|
|
52
|
+
start?: string;
|
|
53
|
+
/** ScrollTrigger end (scrub mode). @default "bottom 45%" */
|
|
54
|
+
end?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function flipWords(from: TargetLike, options: FlipWordsOptions): Destroy {
|
|
58
|
+
initGSAP();
|
|
59
|
+
|
|
60
|
+
const src = one<HTMLElement>(from);
|
|
61
|
+
const dest = one<HTMLElement>(options?.to);
|
|
62
|
+
if (!src || !dest) return () => {};
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
words,
|
|
66
|
+
duration = 1.4,
|
|
67
|
+
ease = "power4.inOut",
|
|
68
|
+
stagger = 0.2,
|
|
69
|
+
scale = 0.2,
|
|
70
|
+
mode = "scroll",
|
|
71
|
+
scrub,
|
|
72
|
+
start = "top 75%",
|
|
73
|
+
end = "bottom 45%",
|
|
74
|
+
} = options;
|
|
75
|
+
|
|
76
|
+
const marked = toArray<HTMLElement>(src.querySelectorAll("[data-flip-word]"));
|
|
77
|
+
const wordEls = words
|
|
78
|
+
? toArray<HTMLElement>(words, src)
|
|
79
|
+
: marked.length
|
|
80
|
+
? marked
|
|
81
|
+
: [...src.children] as HTMLElement[];
|
|
82
|
+
if (!wordEls.length) return () => {};
|
|
83
|
+
|
|
84
|
+
return guard(options, () => {
|
|
85
|
+
// Remember each word's home so destroy() can put everything back in order.
|
|
86
|
+
const home = wordEls.map((w) => ({ parent: w.parentNode, next: w.nextSibling }));
|
|
87
|
+
const prevTransform = wordEls.map((w) => w.style.transform);
|
|
88
|
+
|
|
89
|
+
// Measure in the source, move into the destination, then animate from
|
|
90
|
+
// the measurement — the FLIP. The wrapper timeline owns the flight so
|
|
91
|
+
// ScrollTrigger can scrub it and the scale accents can share its clock.
|
|
92
|
+
const state = Flip.getState(wordEls);
|
|
93
|
+
wordEls.forEach((w) => dest.appendChild(w));
|
|
94
|
+
|
|
95
|
+
const tl = gsap.timeline({ paused: true });
|
|
96
|
+
// The wrapper is paused, and `add` is synchronous, so the Flip child
|
|
97
|
+
// never renders on the global timeline before the wrapper owns it.
|
|
98
|
+
tl.add(Flip.from(state, { duration, ease, stagger: { each: stagger } }), 0);
|
|
99
|
+
|
|
100
|
+
if (scale > 0) {
|
|
101
|
+
wordEls.forEach((w, i) => {
|
|
102
|
+
const at = i * stagger;
|
|
103
|
+
tl.to(w, { scale, duration: duration * 0.5, ease }, at);
|
|
104
|
+
tl.to(w, { scale: 1, duration: duration * 0.5, ease }, at + duration * 0.5);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let st: ScrollTrigger | undefined;
|
|
109
|
+
if (mode === "immediate") {
|
|
110
|
+
tl.play(0);
|
|
111
|
+
} else if (scrub !== undefined) {
|
|
112
|
+
st = ScrollTrigger.create({
|
|
113
|
+
trigger: src,
|
|
114
|
+
start,
|
|
115
|
+
end,
|
|
116
|
+
scrub: scrub === true ? true : scrub,
|
|
117
|
+
animation: tl,
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
st = ScrollTrigger.create({
|
|
121
|
+
trigger: src,
|
|
122
|
+
start,
|
|
123
|
+
animation: tl,
|
|
124
|
+
toggleActions: "play none none reverse",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
ScrollTrigger.refresh();
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
st?.kill();
|
|
131
|
+
tl.kill();
|
|
132
|
+
killTweens(wordEls);
|
|
133
|
+
// Reparent home. A stored `next` sibling that has moved with the words
|
|
134
|
+
// is not in the parent anymore — fall back to appending in order.
|
|
135
|
+
wordEls.forEach((w, i) => {
|
|
136
|
+
const h = home[i];
|
|
137
|
+
if (!h.parent) return;
|
|
138
|
+
const ref = h.next && h.next.parentNode === h.parent ? h.next : null;
|
|
139
|
+
h.parent.insertBefore(w, ref);
|
|
140
|
+
});
|
|
141
|
+
gsap.set(wordEls, { clearProps: "transform" });
|
|
142
|
+
wordEls.forEach((w, i) => {
|
|
143
|
+
w.style.transform = prevTransform[i];
|
|
144
|
+
});
|
|
145
|
+
ScrollTrigger.refresh();
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magnetic hover — buttons and links that pull toward the pointer.
|
|
3
|
+
*
|
|
4
|
+
* While the pointer is over the element it follows the cursor (a fraction
|
|
5
|
+
* of its own box), tilts toward the pull, and optionally grows a touch.
|
|
6
|
+
* On leave it springs back to rest with an elastic snap:
|
|
7
|
+
*
|
|
8
|
+
* magnetic("[data-magnet]", { strength: 0.5, rotation: 10 });
|
|
9
|
+
*
|
|
10
|
+
* Attach it to individual buttons/links (or a container's children — pass
|
|
11
|
+
* the list). destroy() removes the listeners, kills in-flight tweens and
|
|
12
|
+
* restores the inline transform.
|
|
13
|
+
*/
|
|
14
|
+
import { gsap, initGSAP, killTweens } from "../core/gsap.js";
|
|
15
|
+
import { guard } from "../core/guard.js";
|
|
16
|
+
import { toArray } from "../core/util.js";
|
|
17
|
+
import type { CommonOptions, Destroy, TargetLike } from "../core/types.js";
|
|
18
|
+
|
|
19
|
+
export interface MagneticOptions extends CommonOptions {
|
|
20
|
+
/** How far the element follows the pointer — fraction of its own box. @default 0.4 */
|
|
21
|
+
strength?: number;
|
|
22
|
+
/** Max tilt in degrees at full pull (0 disables rotation). @default 8 */
|
|
23
|
+
rotation?: number;
|
|
24
|
+
/** Scale held while the pointer is over the element (1 = none). @default 1 */
|
|
25
|
+
scale?: number;
|
|
26
|
+
/** Spring-back duration, seconds. @default 1.2 */
|
|
27
|
+
duration?: number;
|
|
28
|
+
/** Spring-back ease. @default "elastic.out(1, 0.35)" */
|
|
29
|
+
ease?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function magnetic(target: TargetLike, options: MagneticOptions = {}): Destroy {
|
|
33
|
+
initGSAP();
|
|
34
|
+
|
|
35
|
+
const els = toArray<HTMLElement>(target);
|
|
36
|
+
if (!els.length) return () => {};
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
strength = 0.4,
|
|
40
|
+
rotation = 8,
|
|
41
|
+
scale = 1,
|
|
42
|
+
duration = 1.2,
|
|
43
|
+
ease = "elastic.out(1, 0.35)",
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
return guard(options, () => {
|
|
47
|
+
const prevTransform = els.map((el) => el.style.transform);
|
|
48
|
+
const listeners: Array<{ el: HTMLElement; type: string; fn: EventListener }> = [];
|
|
49
|
+
|
|
50
|
+
els.forEach((el) => {
|
|
51
|
+
const onMove = (e: Event) => {
|
|
52
|
+
const ptr = e as PointerEvent;
|
|
53
|
+
const r = el.getBoundingClientRect();
|
|
54
|
+
const dx = ptr.clientX - (r.left + r.width / 2);
|
|
55
|
+
const dy = ptr.clientY - (r.top + r.height / 2);
|
|
56
|
+
const nx = r.width ? dx / (r.width / 2) : 0; // -1 … 1 across the box
|
|
57
|
+
gsap.to(el, {
|
|
58
|
+
x: dx * strength,
|
|
59
|
+
y: dy * strength,
|
|
60
|
+
rotation: rotation ? nx * rotation : 0,
|
|
61
|
+
scale,
|
|
62
|
+
duration: 0.4,
|
|
63
|
+
ease: "power3.out",
|
|
64
|
+
overwrite: "auto",
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const onLeave = () => {
|
|
68
|
+
gsap.to(el, { x: 0, y: 0, rotation: 0, scale: 1, duration, ease, overwrite: "auto" });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
el.addEventListener("pointermove", onMove);
|
|
72
|
+
el.addEventListener("pointerleave", onLeave);
|
|
73
|
+
listeners.push(
|
|
74
|
+
{ el, type: "pointermove", fn: onMove },
|
|
75
|
+
{ el, type: "pointerleave", fn: onLeave },
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
listeners.forEach(({ el, type, fn }) => el.removeEventListener(type, fn));
|
|
81
|
+
killTweens(els);
|
|
82
|
+
els.forEach((el, i) => {
|
|
83
|
+
el.style.transform = prevTransform[i];
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* mediaSettle("[data-settle]", { from: 1.3 }); // on enter
|
|
9
9
|
* mediaSettle("[data-settle]", { scrub: 0.5 }); // scroll-bound
|
|
10
|
+
* mediaSettle("[data-settle]", { replay: true }); // reverse on leave-back, replay on re-enter
|
|
10
11
|
*
|
|
11
12
|
* destroy() kills the tween and restores the original scale.
|
|
12
13
|
*/
|
|
@@ -34,6 +35,8 @@ export interface MediaSettleOptions extends CommonOptions {
|
|
|
34
35
|
start?: string;
|
|
35
36
|
/** ScrollTrigger end position (scrub mode). @default "bottom top" */
|
|
36
37
|
end?: string;
|
|
38
|
+
/** Re-settle when leaving / re-entering the viewport (enter mode). @default false */
|
|
39
|
+
replay?: boolean;
|
|
37
40
|
/**
|
|
38
41
|
* Bind the settle to scroll progress instead of playing it on enter —
|
|
39
42
|
* number = scrub smoothing seconds, `true` = immediate. unset = one-shot.
|
|
@@ -54,6 +57,7 @@ export function mediaSettle(target: TargetLike, options: MediaSettleOptions = {}
|
|
|
54
57
|
mode = "scroll",
|
|
55
58
|
start = "top 75%",
|
|
56
59
|
end = "bottom top",
|
|
60
|
+
replay = false,
|
|
57
61
|
scrub,
|
|
58
62
|
} = options;
|
|
59
63
|
|
|
@@ -79,7 +83,9 @@ export function mediaSettle(target: TargetLike, options: MediaSettleOptions = {}
|
|
|
79
83
|
vars.duration = duration;
|
|
80
84
|
vars.ease = ease;
|
|
81
85
|
if (mode === "scroll") {
|
|
82
|
-
vars.scrollTrigger =
|
|
86
|
+
vars.scrollTrigger = replay
|
|
87
|
+
? { trigger: els[0], start, toggleActions: "play reverse play reverse" }
|
|
88
|
+
: { trigger: els[0], start, once: true };
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
|
package/src/index.ts
CHANGED
|
@@ -28,11 +28,13 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Text extras:
|
|
30
30
|
* scrambleText() decode / cipher reveal per character
|
|
31
|
+
* flipWords() words that glide between two layouts (FLIP)
|
|
31
32
|
*
|
|
32
33
|
* Micro-interactions:
|
|
33
34
|
* liquidButton() SVG wave flood on hover
|
|
34
35
|
* underlineLink() underline wipe on hover
|
|
35
36
|
* cursorFollower() spring-follow cursor tag
|
|
37
|
+
* magnetic() elements that pull toward the pointer and spring back
|
|
36
38
|
* counter() tabular number ticker (timed or scroll-progress)
|
|
37
39
|
* audioBars() equaliser visualiser
|
|
38
40
|
* themeReveal() View Transitions circular wipe
|
|
@@ -61,7 +63,7 @@ export { unfoldReveal } from "./effects/unfold-reveal.js";
|
|
|
61
63
|
export type { UnfoldRevealOptions } from "./effects/unfold-reveal.js";
|
|
62
64
|
|
|
63
65
|
export { clipWipe } from "./effects/clip-wipe.js";
|
|
64
|
-
export type { ClipWipeOptions } from "./effects/clip-wipe.js";
|
|
66
|
+
export type { ClipWipeOptions, ClipWipeFrom } from "./effects/clip-wipe.js";
|
|
65
67
|
|
|
66
68
|
export { parallax } from "./effects/parallax.js";
|
|
67
69
|
export type { ParallaxOptions } from "./effects/parallax.js";
|
|
@@ -101,6 +103,9 @@ export type { DragStripOptions } from "./effects/drag-strip.js";
|
|
|
101
103
|
export { scrambleText } from "./effects/scramble-text.js";
|
|
102
104
|
export type { ScrambleTextOptions } from "./effects/scramble-text.js";
|
|
103
105
|
|
|
106
|
+
export { flipWords } from "./effects/flip-words.js";
|
|
107
|
+
export type { FlipWordsOptions } from "./effects/flip-words.js";
|
|
108
|
+
|
|
104
109
|
/* ---- micro-interactions ---- */
|
|
105
110
|
export { liquidButton, underlineLink } from "./effects/liquid-button.js";
|
|
106
111
|
export type { LiquidButtonOptions } from "./effects/liquid-button.js";
|
|
@@ -108,6 +113,9 @@ export type { LiquidButtonOptions } from "./effects/liquid-button.js";
|
|
|
108
113
|
export { cursorFollower } from "./effects/cursor-follower.js";
|
|
109
114
|
export type { CursorFollowerOptions } from "./effects/cursor-follower.js";
|
|
110
115
|
|
|
116
|
+
export { magnetic } from "./effects/magnetic.js";
|
|
117
|
+
export type { MagneticOptions } from "./effects/magnetic.js";
|
|
118
|
+
|
|
111
119
|
export { counter, audioBars } from "./effects/counter.js";
|
|
112
120
|
export type { CounterOptions, AudioBarsOptions, AudioBarsHandle } from "./effects/counter.js";
|
|
113
121
|
|