@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.
@@ -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,4 +0,0 @@
1
- export * from "./fade";
2
- export * from "./hero";
3
- export * from "./pinterest";
4
- export * from "./ripple";
@@ -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