@ssgoi/core 0.0.1 → 0.0.2
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/dist/index.cjs +1 -0
- package/dist/index.js +311 -0
- package/dist/view-transitions/index.cjs +1 -0
- package/dist/{ssgoi2.js → view-transitions/index.js} +1 -1
- package/package.json +11 -3
- package/dist/ssgoi2.cjs +0 -1
- package/dist/ssgoi4.cjs +0 -1
- package/dist/ssgoi4.js +0 -764
- package/dist/transitions.d.ts +0 -2
- package/dist/view-transitions.d.ts +0 -2
- package/src/lib/animator.ts +0 -172
- package/src/lib/create-ssgoi-transition-context.ts +0 -265
- package/src/lib/create-transition-callback.ts +0 -246
- package/src/lib/index.ts +0 -3
- package/src/lib/transition.ts +0 -76
- package/src/lib/transitions/blur.ts +0 -55
- package/src/lib/transitions/bounce.ts +0 -61
- package/src/lib/transitions/fade.ts +0 -33
- package/src/lib/transitions/index.ts +0 -7
- package/src/lib/transitions/none.ts +0 -27
- package/src/lib/transitions/rotate.ts +0 -57
- package/src/lib/transitions/scale.ts +0 -34
- package/src/lib/transitions/slide.ts +0 -53
- package/src/lib/types.ts +0 -60
- package/src/lib/utils.ts +0 -39
- package/src/lib/view-transitions/fade.ts +0 -29
- package/src/lib/view-transitions/hero.ts +0 -179
- package/src/lib/view-transitions/index.ts +0 -4
- package/src/lib/view-transitions/pinterest.ts +0 -403
- package/src/lib/view-transitions/ripple.ts +0 -45
- /package/dist/{ssgoi3.cjs → transitions/index.cjs} +0 -0
- /package/dist/{ssgoi3.js → transitions/index.js} +0 -0
- /package/dist/{ssgoi.cjs → types.cjs} +0 -0
- /package/dist/{ssgoi.js → types.js} +0 -0
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import type { SpringConfig, SggoiTransition } from "../types";
|
|
2
|
-
import { prepareOutgoing } from "../utils";
|
|
3
|
-
|
|
4
|
-
interface HeroOptions {
|
|
5
|
-
spring?: Partial<SpringConfig>;
|
|
6
|
-
timeout?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function getHeroEl(page: HTMLElement, key: string): HTMLElement | null {
|
|
10
|
-
return page.querySelector(`[data-hero-key="${key}"]`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getRect(root: HTMLElement, el: HTMLElement): DOMRect {
|
|
14
|
-
const rootRect = root.getBoundingClientRect();
|
|
15
|
-
const elRect = el.getBoundingClientRect();
|
|
16
|
-
|
|
17
|
-
return new DOMRect(
|
|
18
|
-
elRect.left - rootRect.left,
|
|
19
|
-
elRect.top - rootRect.top,
|
|
20
|
-
elRect.width,
|
|
21
|
-
elRect.height
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const hero = (options: HeroOptions = {}): SggoiTransition => {
|
|
26
|
-
const spring: SpringConfig = {
|
|
27
|
-
stiffness: options.spring?.stiffness ?? 300,
|
|
28
|
-
damping: options.spring?.damping ?? 30,
|
|
29
|
-
};
|
|
30
|
-
const timeout = options.timeout ?? 300;
|
|
31
|
-
|
|
32
|
-
// Closure variables to share state between in/out
|
|
33
|
-
let fromNode: HTMLElement | null = null;
|
|
34
|
-
let resolver: ((value: boolean) => void) | null = null;
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
in: async (element, context) => {
|
|
38
|
-
const toNode = element;
|
|
39
|
-
|
|
40
|
-
// Find all hero elements in the incoming page
|
|
41
|
-
const heroEls = Array.from(toNode.querySelectorAll("[data-hero-key]"));
|
|
42
|
-
if (heroEls.length === 0) {
|
|
43
|
-
return {
|
|
44
|
-
spring,
|
|
45
|
-
tick: () => {}, // No hero elements, skip animation
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Wait for fromNode to be set by out transition
|
|
50
|
-
const hasFromNode = await new Promise<boolean>((resolve) => {
|
|
51
|
-
if (fromNode) {
|
|
52
|
-
// fromNode already set by out transition
|
|
53
|
-
resolve(true);
|
|
54
|
-
} else {
|
|
55
|
-
// Store resolver for out transition to call
|
|
56
|
-
resolver = resolve;
|
|
57
|
-
// Timeout fallback
|
|
58
|
-
setTimeout(() => {
|
|
59
|
-
resolver = null;
|
|
60
|
-
resolve(false);
|
|
61
|
-
}, timeout);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (!hasFromNode || !fromNode) {
|
|
66
|
-
fromNode = null;
|
|
67
|
-
return {
|
|
68
|
-
spring,
|
|
69
|
-
tick: () => {}, // No fromNode, skip animation
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Calculate animations for matching hero elements
|
|
74
|
-
const heroAnimations = heroEls
|
|
75
|
-
.map((heroEl) => {
|
|
76
|
-
const key = heroEl.getAttribute("data-hero-key");
|
|
77
|
-
if (!key) return null;
|
|
78
|
-
|
|
79
|
-
const fromEl = getHeroEl(fromNode!, key);
|
|
80
|
-
if (!fromEl) return null;
|
|
81
|
-
|
|
82
|
-
const toEl = heroEl as HTMLElement;
|
|
83
|
-
|
|
84
|
-
// Calculate animation parameters
|
|
85
|
-
const fromRect = getRect(fromNode!, fromEl);
|
|
86
|
-
const toRect = getRect(toNode, toEl);
|
|
87
|
-
const dx = fromRect.left - toRect.left - context.scrollOffset.x;
|
|
88
|
-
const dy = fromRect.top - toRect.top - context.scrollOffset.y;
|
|
89
|
-
const dw = fromRect.width / toRect.width;
|
|
90
|
-
const dh = fromRect.height / toRect.height;
|
|
91
|
-
|
|
92
|
-
// Store original styles
|
|
93
|
-
const originalTransform = toEl.style.transform;
|
|
94
|
-
const originalPosition = toEl.style.position;
|
|
95
|
-
const originalTransformOrigin = toEl.style.transformOrigin;
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
toEl,
|
|
99
|
-
dx,
|
|
100
|
-
dy,
|
|
101
|
-
dw,
|
|
102
|
-
dh,
|
|
103
|
-
originalTransform,
|
|
104
|
-
originalPosition,
|
|
105
|
-
originalTransformOrigin,
|
|
106
|
-
};
|
|
107
|
-
})
|
|
108
|
-
.filter(Boolean) as Array<{
|
|
109
|
-
toEl: HTMLElement;
|
|
110
|
-
dx: number;
|
|
111
|
-
dy: number;
|
|
112
|
-
dw: number;
|
|
113
|
-
dh: number;
|
|
114
|
-
originalTransform: string;
|
|
115
|
-
originalPosition: string;
|
|
116
|
-
originalTransformOrigin: string;
|
|
117
|
-
}>;
|
|
118
|
-
|
|
119
|
-
// Reset fromNode for next transition
|
|
120
|
-
fromNode = null;
|
|
121
|
-
|
|
122
|
-
if (heroAnimations.length === 0) {
|
|
123
|
-
return {
|
|
124
|
-
spring,
|
|
125
|
-
tick: () => {}, // No matching hero elements
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
spring,
|
|
131
|
-
prepare: () => {
|
|
132
|
-
heroAnimations.forEach(({ toEl }) => {
|
|
133
|
-
toEl.style.position = "relative";
|
|
134
|
-
toEl.style.transformOrigin = "top left";
|
|
135
|
-
});
|
|
136
|
-
},
|
|
137
|
-
tick: (progress) => {
|
|
138
|
-
// Animate all hero elements
|
|
139
|
-
heroAnimations.forEach(({ toEl, dx, dy, dw, dh }) => {
|
|
140
|
-
toEl.style.transform = `translate(${(1 - progress) * dx}px,${(1 - progress) * dy}px) scale(${progress + (1 - progress) * dw}, ${progress + (1 - progress) * dh})`;
|
|
141
|
-
});
|
|
142
|
-
},
|
|
143
|
-
onEnd: () => {
|
|
144
|
-
// Reset all hero elements
|
|
145
|
-
heroAnimations.forEach(
|
|
146
|
-
({
|
|
147
|
-
toEl,
|
|
148
|
-
originalTransform,
|
|
149
|
-
originalPosition,
|
|
150
|
-
originalTransformOrigin,
|
|
151
|
-
}) => {
|
|
152
|
-
toEl.style.transform = originalTransform;
|
|
153
|
-
toEl.style.position = originalPosition;
|
|
154
|
-
toEl.style.transformOrigin = originalTransformOrigin;
|
|
155
|
-
}
|
|
156
|
-
);
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
},
|
|
160
|
-
out: async (element) => {
|
|
161
|
-
return {
|
|
162
|
-
onStart: () => {
|
|
163
|
-
// Store fromNode
|
|
164
|
-
fromNode = element;
|
|
165
|
-
|
|
166
|
-
// If there's a waiting resolver, resolve it
|
|
167
|
-
if (resolver) {
|
|
168
|
-
resolver(true);
|
|
169
|
-
resolver = null;
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
prepare: (element) => {
|
|
173
|
-
prepareOutgoing(element);
|
|
174
|
-
element.style.opacity = "0";
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
};
|
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
import type { SpringConfig, SggoiTransition } from "../types";
|
|
2
|
-
import { prepareOutgoing } from "../utils";
|
|
3
|
-
|
|
4
|
-
interface PinterestOptions {
|
|
5
|
-
spring?: Partial<SpringConfig>;
|
|
6
|
-
timeout?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Pinterest-style transition for gallery/detail views
|
|
11
|
-
*
|
|
12
|
-
* Scenarios:
|
|
13
|
-
* 1. Gallery → Detail (enter mode):
|
|
14
|
-
* - Gallery item expands to fill the screen with clip-path animation
|
|
15
|
-
* - From: Small gallery item position
|
|
16
|
-
* - To: Full detail view
|
|
17
|
-
*
|
|
18
|
-
* 2. Detail → Gallery (exit mode):
|
|
19
|
-
* - Detail view shrinks down to gallery item position
|
|
20
|
-
* - From: Full detail view
|
|
21
|
-
* - To: Small gallery item position
|
|
22
|
-
*
|
|
23
|
-
* Usage:
|
|
24
|
-
* - Gallery page: Use data-pinterest-gallery-key="unique-id"
|
|
25
|
-
* - Detail page: Use data-pinterest-detail-key="unique-id"
|
|
26
|
-
* - The transition auto-detects the mode based on which keys match between pages
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
function getRect(root: HTMLElement, el: HTMLElement): DOMRect {
|
|
30
|
-
const rootRect = root.getBoundingClientRect();
|
|
31
|
-
const elRect = el.getBoundingClientRect();
|
|
32
|
-
|
|
33
|
-
return new DOMRect(
|
|
34
|
-
elRect.left - rootRect.left,
|
|
35
|
-
elRect.top - rootRect.top,
|
|
36
|
-
elRect.width,
|
|
37
|
-
elRect.height
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type AnimationFunc = (progress: number) => void;
|
|
42
|
-
|
|
43
|
-
// Animation creators for each transition type
|
|
44
|
-
function createDetailIn(
|
|
45
|
-
{
|
|
46
|
-
detailRect: fromRect,
|
|
47
|
-
galleryRect: toRect,
|
|
48
|
-
pageRect,
|
|
49
|
-
scrollOffset,
|
|
50
|
-
}: {
|
|
51
|
-
detailRect: DOMRect;
|
|
52
|
-
galleryRect: DOMRect;
|
|
53
|
-
pageRect: DOMRect;
|
|
54
|
-
scrollOffset: { x: number; y: number };
|
|
55
|
-
},
|
|
56
|
-
node: HTMLElement
|
|
57
|
-
): AnimationFunc {
|
|
58
|
-
// 시작 위치 (from)와 끝 위치 (to) 사이의 거리 계산
|
|
59
|
-
const dx =
|
|
60
|
-
toRect.left -
|
|
61
|
-
fromRect.left +
|
|
62
|
-
(toRect.width - fromRect.width) / 2 -
|
|
63
|
-
scrollOffset.x;
|
|
64
|
-
const dy =
|
|
65
|
-
toRect.top -
|
|
66
|
-
fromRect.top +
|
|
67
|
-
(toRect.height - fromRect.height) / 2 -
|
|
68
|
-
scrollOffset.y;
|
|
69
|
-
|
|
70
|
-
// scale 계산
|
|
71
|
-
const scaleX = toRect.width / fromRect.width;
|
|
72
|
-
const scaleY = toRect.height / fromRect.height;
|
|
73
|
-
const scale = Math.max(scaleX, scaleY);
|
|
74
|
-
|
|
75
|
-
// clip-path 계산
|
|
76
|
-
const startTop = (fromRect.top / pageRect.height) * 100;
|
|
77
|
-
const startRight =
|
|
78
|
-
((pageRect.width - (fromRect.left + fromRect.width)) / pageRect.width) *
|
|
79
|
-
100;
|
|
80
|
-
const startBottom =
|
|
81
|
-
((pageRect.height - (fromRect.top + fromRect.height)) / pageRect.height) *
|
|
82
|
-
100;
|
|
83
|
-
const startLeft = (fromRect.left / pageRect.width) * 100;
|
|
84
|
-
|
|
85
|
-
node.style.transformOrigin = `${fromRect.left + fromRect.width / 2}px ${fromRect.top + fromRect.height / 2}px`;
|
|
86
|
-
return (progress: number) => {
|
|
87
|
-
const u = 1 - progress;
|
|
88
|
-
const currentTop = startTop * u;
|
|
89
|
-
const currentRight = startRight * u;
|
|
90
|
-
const currentBottom = startBottom * u;
|
|
91
|
-
const currentLeft = startLeft * u;
|
|
92
|
-
|
|
93
|
-
node.style.clipPath = `inset(${currentTop}% ${currentRight}% ${currentBottom}% ${currentLeft}%)`;
|
|
94
|
-
node.style.transform = `translate(${dx * u}px, ${dy * u}px) scale(${1 + (scale - 1) * u})`;
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function createGalleryOut(
|
|
99
|
-
{
|
|
100
|
-
galleryRect: fromRect,
|
|
101
|
-
detailRect: toRect,
|
|
102
|
-
scrollOffset,
|
|
103
|
-
}: {
|
|
104
|
-
galleryRect: DOMRect;
|
|
105
|
-
detailRect: DOMRect;
|
|
106
|
-
scrollOffset: { x: number; y: number };
|
|
107
|
-
},
|
|
108
|
-
node: HTMLElement
|
|
109
|
-
): AnimationFunc {
|
|
110
|
-
// 시작 위치 (from)와 끝 위치 (to) 사이의 거리 계산
|
|
111
|
-
const dx =
|
|
112
|
-
toRect.left -
|
|
113
|
-
fromRect.left +
|
|
114
|
-
(toRect.width - fromRect.width) / 2 +
|
|
115
|
-
scrollOffset.x;
|
|
116
|
-
const dy =
|
|
117
|
-
toRect.top -
|
|
118
|
-
fromRect.top +
|
|
119
|
-
(toRect.height - fromRect.height) / 2 +
|
|
120
|
-
scrollOffset.y;
|
|
121
|
-
|
|
122
|
-
// scale 계산
|
|
123
|
-
const scaleX = toRect.width / fromRect.width;
|
|
124
|
-
const scaleY = toRect.height / fromRect.height;
|
|
125
|
-
const scale = Math.max(scaleX, scaleY);
|
|
126
|
-
|
|
127
|
-
return (progress: number) => {
|
|
128
|
-
node.style.transformOrigin = `${fromRect.left + fromRect.width / 2}px ${fromRect.top + fromRect.height / 2}px`;
|
|
129
|
-
const t = 1 - progress; // 0 -> 1
|
|
130
|
-
node.style.transform = `translate(${dx * t - scrollOffset.x}px, ${dy * t - scrollOffset.y}px) scale(${1 + (scale - 1) * t})`;
|
|
131
|
-
node.style.opacity = `${1 - t}`;
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function createGalleryIn(
|
|
136
|
-
{
|
|
137
|
-
galleryRect: fromRect,
|
|
138
|
-
detailRect: toRect,
|
|
139
|
-
scrollOffset,
|
|
140
|
-
}: {
|
|
141
|
-
galleryRect: DOMRect;
|
|
142
|
-
detailRect: DOMRect;
|
|
143
|
-
scrollOffset: { x: number; y: number };
|
|
144
|
-
},
|
|
145
|
-
node: HTMLElement
|
|
146
|
-
): AnimationFunc {
|
|
147
|
-
const dx =
|
|
148
|
-
toRect.left -
|
|
149
|
-
fromRect.left +
|
|
150
|
-
(toRect.width - fromRect.width) / 2 -
|
|
151
|
-
scrollOffset.x;
|
|
152
|
-
const dy =
|
|
153
|
-
toRect.top -
|
|
154
|
-
fromRect.top +
|
|
155
|
-
(toRect.height - fromRect.height) / 2 -
|
|
156
|
-
scrollOffset.y;
|
|
157
|
-
|
|
158
|
-
// scale 계산
|
|
159
|
-
const scaleX = toRect.width / fromRect.width;
|
|
160
|
-
const scaleY = toRect.height / fromRect.height;
|
|
161
|
-
const scale = Math.max(scaleX, scaleY);
|
|
162
|
-
|
|
163
|
-
return (progress: number) => {
|
|
164
|
-
const t = 1 - progress; // 1 -> 0;
|
|
165
|
-
const u = progress; // 0 -> 1;
|
|
166
|
-
node.style.transformOrigin = `${fromRect.left + fromRect.width / 2}px ${fromRect.top + fromRect.height / 2}px`;
|
|
167
|
-
node.style.transform = `translate(${dx * t}px, ${dy * t}px) scale(${1 + (scale - 1) * t})`;
|
|
168
|
-
node.style.opacity = `${u}`;
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function createDetailOut(
|
|
173
|
-
{
|
|
174
|
-
detailRect: fromRect,
|
|
175
|
-
galleryRect: toRect,
|
|
176
|
-
pageRect,
|
|
177
|
-
scrollOffset,
|
|
178
|
-
}: {
|
|
179
|
-
detailRect: DOMRect;
|
|
180
|
-
galleryRect: DOMRect;
|
|
181
|
-
pageRect: DOMRect;
|
|
182
|
-
scrollOffset: { x: number; y: number };
|
|
183
|
-
},
|
|
184
|
-
node: HTMLElement
|
|
185
|
-
): AnimationFunc {
|
|
186
|
-
// 시작 위치 (from)와 끝 위치 (to) 사이의 거리 계산
|
|
187
|
-
const dx =
|
|
188
|
-
toRect.left -
|
|
189
|
-
fromRect.left +
|
|
190
|
-
(toRect.width - fromRect.width) / 2 +
|
|
191
|
-
scrollOffset.x;
|
|
192
|
-
const dy =
|
|
193
|
-
toRect.top -
|
|
194
|
-
fromRect.top +
|
|
195
|
-
(toRect.height - fromRect.height) / 2 +
|
|
196
|
-
scrollOffset.y;
|
|
197
|
-
|
|
198
|
-
// scale 계산
|
|
199
|
-
const scaleX = toRect.width / fromRect.width;
|
|
200
|
-
const scaleY = toRect.height / fromRect.height;
|
|
201
|
-
const scale = Math.min(scaleX, scaleY);
|
|
202
|
-
|
|
203
|
-
// clip-path 계산
|
|
204
|
-
const startTop = (fromRect.top / pageRect.height) * 100;
|
|
205
|
-
const startRight =
|
|
206
|
-
((pageRect.width - (fromRect.left + fromRect.width)) / pageRect.width) *
|
|
207
|
-
100;
|
|
208
|
-
const startBottom =
|
|
209
|
-
((pageRect.height - (fromRect.top + fromRect.height)) / pageRect.height) *
|
|
210
|
-
100;
|
|
211
|
-
const startLeft = (fromRect.left / pageRect.width) * 100;
|
|
212
|
-
node.style.transformOrigin = `${fromRect.left + fromRect.width / 2}px ${fromRect.top + fromRect.height / 2}px`;
|
|
213
|
-
return (progress: number) => {
|
|
214
|
-
const t = 1 - progress; // 0 -> 1 for out transitions
|
|
215
|
-
const currentTop = startTop * t;
|
|
216
|
-
const currentRight = startRight * t;
|
|
217
|
-
const currentBottom = startBottom * t;
|
|
218
|
-
const currentLeft = startLeft * t;
|
|
219
|
-
|
|
220
|
-
node.style.clipPath = `inset(${currentTop}% ${currentRight}% ${currentBottom}% ${currentLeft}%)`;
|
|
221
|
-
node.style.transform = `translate(${dx * t - scrollOffset.x}px, ${dy * t - scrollOffset.y}px) scale(${1 + (scale - 1) * t})`;
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
interface AnimationHandlers {
|
|
226
|
-
inAnimation: AnimationFunc;
|
|
227
|
-
outAnimation: AnimationFunc;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function createAnimationConfig(
|
|
231
|
-
fromNode: HTMLElement,
|
|
232
|
-
toNode: HTMLElement,
|
|
233
|
-
scrollOffset: { x: number; y: number }
|
|
234
|
-
): AnimationHandlers | null {
|
|
235
|
-
// Find detail element first (only one per page)
|
|
236
|
-
const fromDetail = fromNode.querySelector(
|
|
237
|
-
"[data-pinterest-detail-key]"
|
|
238
|
-
) as HTMLElement | null;
|
|
239
|
-
const toDetail = toNode.querySelector(
|
|
240
|
-
"[data-pinterest-detail-key]"
|
|
241
|
-
) as HTMLElement | null;
|
|
242
|
-
|
|
243
|
-
// Early return if multiple details on either page
|
|
244
|
-
if (
|
|
245
|
-
fromNode.querySelectorAll("[data-pinterest-detail-key]").length > 1 ||
|
|
246
|
-
toNode.querySelectorAll("[data-pinterest-detail-key]").length > 1
|
|
247
|
-
) {
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
let galleryEl: HTMLElement | null = null;
|
|
252
|
-
let detailEl: HTMLElement | null = null;
|
|
253
|
-
let isEnterMode = false;
|
|
254
|
-
|
|
255
|
-
// Case 1: Gallery → Detail (enter mode)
|
|
256
|
-
if (!fromDetail && toDetail) {
|
|
257
|
-
detailEl = toDetail;
|
|
258
|
-
const key = detailEl.getAttribute("data-pinterest-detail-key");
|
|
259
|
-
if (!key) return null;
|
|
260
|
-
|
|
261
|
-
// Find matching gallery in from page
|
|
262
|
-
galleryEl = fromNode.querySelector(
|
|
263
|
-
`[data-pinterest-gallery-key="${key}"]`
|
|
264
|
-
) as HTMLElement | null;
|
|
265
|
-
|
|
266
|
-
if (galleryEl) {
|
|
267
|
-
isEnterMode = true;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
// Case 2: Detail → Gallery (exit mode)
|
|
271
|
-
else if (fromDetail && !toDetail) {
|
|
272
|
-
detailEl = fromDetail;
|
|
273
|
-
const key = detailEl.getAttribute("data-pinterest-detail-key");
|
|
274
|
-
if (!key) return null;
|
|
275
|
-
|
|
276
|
-
// Find matching gallery in to page
|
|
277
|
-
galleryEl = toNode.querySelector(
|
|
278
|
-
`[data-pinterest-gallery-key="${key}"]`
|
|
279
|
-
) as HTMLElement | null;
|
|
280
|
-
|
|
281
|
-
if (galleryEl) {
|
|
282
|
-
isEnterMode = false;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (!galleryEl || !detailEl) {
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Calculate rects based on mode
|
|
291
|
-
const galleryRect = getRect(isEnterMode ? fromNode : toNode, galleryEl);
|
|
292
|
-
const detailRect = getRect(isEnterMode ? toNode : fromNode, detailEl);
|
|
293
|
-
const pageRect = toNode.getBoundingClientRect();
|
|
294
|
-
|
|
295
|
-
// Return appropriate animation functions based on mode
|
|
296
|
-
if (isEnterMode) {
|
|
297
|
-
return {
|
|
298
|
-
inAnimation: createDetailIn(
|
|
299
|
-
{ detailRect, galleryRect, pageRect, scrollOffset },
|
|
300
|
-
toNode
|
|
301
|
-
),
|
|
302
|
-
outAnimation: createGalleryOut(
|
|
303
|
-
{ galleryRect, detailRect, scrollOffset },
|
|
304
|
-
fromNode
|
|
305
|
-
),
|
|
306
|
-
};
|
|
307
|
-
} else {
|
|
308
|
-
return {
|
|
309
|
-
inAnimation: createGalleryIn(
|
|
310
|
-
{ galleryRect, detailRect, scrollOffset },
|
|
311
|
-
toNode
|
|
312
|
-
),
|
|
313
|
-
outAnimation: createDetailOut(
|
|
314
|
-
{ detailRect, galleryRect, pageRect, scrollOffset },
|
|
315
|
-
fromNode
|
|
316
|
-
),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export const pinterest = (options: PinterestOptions = {}): SggoiTransition => {
|
|
322
|
-
const spring: SpringConfig = {
|
|
323
|
-
stiffness: options.spring?.stiffness ?? 50,
|
|
324
|
-
damping: options.spring?.damping ?? 10,
|
|
325
|
-
};
|
|
326
|
-
const timeout = options.timeout ?? 300;
|
|
327
|
-
|
|
328
|
-
// Closure variables to share state between in/out
|
|
329
|
-
let fromNode: HTMLElement | null = null;
|
|
330
|
-
let resolver: ((value: boolean) => void) | null = null;
|
|
331
|
-
let handlers: AnimationHandlers | null = null;
|
|
332
|
-
|
|
333
|
-
return {
|
|
334
|
-
in: async (element, { scrollOffset }) => {
|
|
335
|
-
const toNode = element;
|
|
336
|
-
|
|
337
|
-
// Wait for fromNode to be set by out transition
|
|
338
|
-
const hasFromNode = await new Promise<boolean>((resolve) => {
|
|
339
|
-
if (fromNode) {
|
|
340
|
-
// fromNode already set by out transition
|
|
341
|
-
resolve(true);
|
|
342
|
-
} else {
|
|
343
|
-
// Store resolver for out transition to call
|
|
344
|
-
resolver = resolve;
|
|
345
|
-
// Timeout fallback
|
|
346
|
-
setTimeout(() => {
|
|
347
|
-
resolver = null;
|
|
348
|
-
resolve(false);
|
|
349
|
-
}, timeout);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
if (!hasFromNode || !fromNode) {
|
|
354
|
-
fromNode = null;
|
|
355
|
-
return {
|
|
356
|
-
spring,
|
|
357
|
-
tick: () => {},
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Detect and prepare animation handlers with saved scrollOffset
|
|
362
|
-
handlers = createAnimationConfig(fromNode, toNode, scrollOffset);
|
|
363
|
-
if (!handlers) {
|
|
364
|
-
return {
|
|
365
|
-
spring,
|
|
366
|
-
tick: () => {},
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Reset fromNode for next transition
|
|
371
|
-
fromNode = null;
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
spring,
|
|
375
|
-
tick: (progress) => {
|
|
376
|
-
if (handlers) handlers.inAnimation(progress);
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
},
|
|
380
|
-
out: async (element) => {
|
|
381
|
-
return {
|
|
382
|
-
spring,
|
|
383
|
-
onStart: () => {
|
|
384
|
-
// Store fromNode
|
|
385
|
-
fromNode = element;
|
|
386
|
-
|
|
387
|
-
// If there's a waiting resolver, resolve it
|
|
388
|
-
if (resolver) {
|
|
389
|
-
resolver(true);
|
|
390
|
-
resolver = null;
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
prepare: (element) => {
|
|
394
|
-
prepareOutgoing(element);
|
|
395
|
-
element.style.zIndex = "-1";
|
|
396
|
-
},
|
|
397
|
-
tick: (progress) => {
|
|
398
|
-
if (handlers) handlers.outAnimation(progress);
|
|
399
|
-
},
|
|
400
|
-
};
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { SpringConfig, SggoiTransition } from "../types";
|
|
2
|
-
import { prepareOutgoing } from "../utils";
|
|
3
|
-
|
|
4
|
-
interface RippleOptions {
|
|
5
|
-
spring?: Partial<SpringConfig>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function getOffset(element: Element) {
|
|
9
|
-
const rect = element.getBoundingClientRect();
|
|
10
|
-
const totalHeight = element.clientHeight;
|
|
11
|
-
const visibleHeight = window.innerHeight - rect.top;
|
|
12
|
-
const offset = (50 * visibleHeight) / totalHeight;
|
|
13
|
-
|
|
14
|
-
return Math.min(Math.max(offset, 0), 100);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const ripple = (options: RippleOptions = {}): SggoiTransition => {
|
|
18
|
-
const spring: SpringConfig = {
|
|
19
|
-
stiffness: options.spring?.stiffness ?? 300,
|
|
20
|
-
damping: options.spring?.damping ?? 200,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
in: (element) => {
|
|
25
|
-
const offset = getOffset(element);
|
|
26
|
-
return {
|
|
27
|
-
spring,
|
|
28
|
-
tick: (progress) => {
|
|
29
|
-
element.style.clipPath = `circle(${progress * 100}% at 50% ${offset}%)`;
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
},
|
|
33
|
-
out: (element) => {
|
|
34
|
-
const offset = getOffset(element);
|
|
35
|
-
return {
|
|
36
|
-
spring,
|
|
37
|
-
tick: (progress) => {
|
|
38
|
-
element.style.clipPath = `circle(${progress * 100}% at 50% ${offset}%)`;
|
|
39
|
-
element.style.zIndex = "100";
|
|
40
|
-
},
|
|
41
|
-
prepare: prepareOutgoing,
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|