@ssgoi/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +34 -0
  2. package/dist/animator.d.ts +41 -0
  3. package/dist/animator.d.ts.map +1 -0
  4. package/dist/create-ssgoi-transition-context.d.ts +15 -0
  5. package/dist/create-ssgoi-transition-context.d.ts.map +1 -0
  6. package/dist/create-transition-callback.d.ts +36 -0
  7. package/dist/create-transition-callback.d.ts.map +1 -0
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/ssgoi.cjs +1 -0
  11. package/dist/ssgoi.js +1 -0
  12. package/dist/ssgoi2.cjs +1 -0
  13. package/dist/ssgoi2.js +305 -0
  14. package/dist/ssgoi3.cjs +1 -0
  15. package/dist/ssgoi3.js +173 -0
  16. package/dist/ssgoi4.cjs +1 -0
  17. package/dist/ssgoi4.js +764 -0
  18. package/dist/transition.d.ts +16 -0
  19. package/dist/transition.d.ts.map +1 -0
  20. package/dist/transitions/blur.d.ts +27 -0
  21. package/dist/transitions/blur.d.ts.map +1 -0
  22. package/dist/transitions/bounce.d.ts +27 -0
  23. package/dist/transitions/bounce.d.ts.map +1 -0
  24. package/dist/transitions/fade.d.ts +26 -0
  25. package/dist/transitions/fade.d.ts.map +1 -0
  26. package/dist/transitions/index.d.ts +8 -0
  27. package/dist/transitions/index.d.ts.map +1 -0
  28. package/dist/transitions/none.d.ts +24 -0
  29. package/dist/transitions/none.d.ts.map +1 -0
  30. package/dist/transitions/rotate.d.ts +28 -0
  31. package/dist/transitions/rotate.d.ts.map +1 -0
  32. package/dist/transitions/scale.d.ts +27 -0
  33. package/dist/transitions/scale.d.ts.map +1 -0
  34. package/dist/transitions/slide.d.ts +27 -0
  35. package/dist/transitions/slide.d.ts.map +1 -0
  36. package/dist/transitions.d.ts +2 -0
  37. package/dist/types.d.ts +38 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/utils-BSgyev81.cjs +1 -0
  40. package/dist/utils-DR41YsAh.js +16 -0
  41. package/dist/utils.d.ts +11 -0
  42. package/dist/utils.d.ts.map +1 -0
  43. package/dist/view-transitions/fade.d.ts +7 -0
  44. package/dist/view-transitions/fade.d.ts.map +1 -0
  45. package/dist/view-transitions/hero.d.ts +8 -0
  46. package/dist/view-transitions/hero.d.ts.map +1 -0
  47. package/dist/view-transitions/index.d.ts +5 -0
  48. package/dist/view-transitions/index.d.ts.map +1 -0
  49. package/dist/view-transitions/pinterest.d.ts +8 -0
  50. package/dist/view-transitions/pinterest.d.ts.map +1 -0
  51. package/dist/view-transitions/ripple.d.ts +7 -0
  52. package/dist/view-transitions/ripple.d.ts.map +1 -0
  53. package/dist/view-transitions.d.ts +2 -0
  54. package/package.json +58 -0
  55. package/src/lib/animator.ts +172 -0
  56. package/src/lib/create-ssgoi-transition-context.ts +265 -0
  57. package/src/lib/create-transition-callback.ts +246 -0
  58. package/src/lib/index.ts +3 -0
  59. package/src/lib/transition.ts +76 -0
  60. package/src/lib/transitions/blur.ts +55 -0
  61. package/src/lib/transitions/bounce.ts +61 -0
  62. package/src/lib/transitions/fade.ts +33 -0
  63. package/src/lib/transitions/index.ts +7 -0
  64. package/src/lib/transitions/none.ts +27 -0
  65. package/src/lib/transitions/rotate.ts +57 -0
  66. package/src/lib/transitions/scale.ts +34 -0
  67. package/src/lib/transitions/slide.ts +53 -0
  68. package/src/lib/types.ts +60 -0
  69. package/src/lib/utils.ts +39 -0
  70. package/src/lib/view-transitions/fade.ts +29 -0
  71. package/src/lib/view-transitions/hero.ts +179 -0
  72. package/src/lib/view-transitions/index.ts +4 -0
  73. package/src/lib/view-transitions/pinterest.ts +403 -0
  74. package/src/lib/view-transitions/ripple.ts +45 -0
@@ -0,0 +1,265 @@
1
+ import type {
2
+ SsgoiConfig,
3
+ SsgoiContext,
4
+ GetTransitionConfig,
5
+ Transition,
6
+ } from "./types";
7
+ import { getScrollingElement } from "./utils";
8
+
9
+ /**
10
+ * SSGOI Transition Context Operation Principles
11
+ *
12
+ * Page transition scenario: /home → /about
13
+ *
14
+ * 1. OUT animation starts (when /home page disappears)
15
+ * - getTransition('unique-id', 'out', '/home') is called
16
+ * - Stores { from: '/home' } in pendingTransitions
17
+ * - Creates Promise and stores outResolve (not resolved yet)
18
+ * - Calls checkAndResolve → waits because 'to' is missing
19
+ *
20
+ * 2. IN animation starts (when /about page appears)
21
+ * - getTransition('unique-id', 'in', '/about') is called
22
+ * - Adds { to: '/about' } to existing pending
23
+ * - Creates Promise and stores inResolve
24
+ * - Calls checkAndResolve → both 'from' and 'to' are present!
25
+ *
26
+ * 3. Transition matching and resolution
27
+ * - Finds appropriate transition with from: '/home', to: '/about'
28
+ * - Resolves both out and in with the found transition's settings
29
+ * - Removes the id from pendingTransitions
30
+ *
31
+ * Key point: OUT and IN wait for each other. When both are ready,
32
+ * they find the appropriate transition using from/to info
33
+ * and resolve simultaneously.
34
+ *
35
+ * Edge cases:
36
+ * - No OUT animation on page refresh or initial entry
37
+ * - When only IN is called, checkAndResolve doesn't work without 'from'
38
+ * - Promise isn't resolved, so animation doesn't start
39
+ */
40
+
41
+ type PendingTransition = {
42
+ from?: string;
43
+ to?: string;
44
+ outResolve?: (transition: GetTransitionConfig) => void;
45
+ inResolve?: (transition: GetTransitionConfig) => void;
46
+ };
47
+
48
+ /**
49
+ * Creates a transition configuration
50
+ *
51
+ * @example
52
+ * const config = createSggoiTransitionConfig({
53
+ * transitions: [
54
+ * { from: '/home', to: '/about', transition: fade() },
55
+ * { from: '/products', to: '/products/*', transition: slide() }
56
+ * ],
57
+ * defaultTransition: fade()
58
+ * });
59
+ */
60
+ export function createSggoiTransitionContext(
61
+ options: SsgoiConfig
62
+ ): SsgoiContext {
63
+ let pendingTransition: PendingTransition | null = null;
64
+
65
+ // Process symmetric transitions - creates bidirectional transitions automatically
66
+ const processedTransitions = [...options.transitions];
67
+ const symmetricTransitions: typeof options.transitions = [];
68
+
69
+ for (const transitionDef of options.transitions) {
70
+ if (transitionDef.symmetric) {
71
+ // Check if reverse transition already exists to avoid duplicates
72
+ const reverseExists = processedTransitions.some(
73
+ t => t.from === transitionDef.to && t.to === transitionDef.from
74
+ );
75
+
76
+ if (!reverseExists) {
77
+ // Create reverse transition for symmetric navigation
78
+ symmetricTransitions.push({
79
+ from: transitionDef.to,
80
+ to: transitionDef.from,
81
+ transition: transitionDef.transition,
82
+ // Don't add symmetric flag to the reverse to avoid infinite loop
83
+ });
84
+ }
85
+ }
86
+ }
87
+
88
+ // Add symmetric transitions to the processed list for matching
89
+ processedTransitions.push(...symmetricTransitions);
90
+
91
+ // Scroll tracking - preserves scroll positions between page transitions
92
+ let scrollContainer: HTMLElement | null = null;
93
+ const scrollPositions: Map<string, { x: number; y: number }> = new Map();
94
+ let currentPath: string | null = null;
95
+
96
+ // Scroll listener - captures current scroll position
97
+ const scrollListener = () => {
98
+ if (scrollContainer && currentPath) {
99
+ scrollPositions.set(currentPath, {
100
+ x: scrollContainer.scrollLeft,
101
+ y: scrollContainer.scrollTop,
102
+ });
103
+ }
104
+ };
105
+
106
+ // Start tracking scroll for a path - initializes scroll container and updates current path
107
+ const startScrollTracking = (element: HTMLElement, path: string) => {
108
+ // Initialize scroll container once - finds the scrollable element
109
+ if (!scrollContainer) {
110
+ scrollContainer = getScrollingElement(element);
111
+ scrollContainer.addEventListener("scroll", scrollListener, {
112
+ passive: true,
113
+ });
114
+ }
115
+
116
+ // Update current path for scroll position tracking
117
+ currentPath = path;
118
+ };
119
+
120
+ // Calculate scroll offset - computes difference between pages' scroll positions
121
+ const calculateScrollOffset = (): { x: number; y: number } => {
122
+ const from = pendingTransition?.from;
123
+ const to = pendingTransition?.to;
124
+
125
+ const fromScroll =
126
+ from && scrollPositions.has(from)
127
+ ? scrollPositions.get(from)!
128
+ : { x: 0, y: 0 };
129
+
130
+ const toScroll =
131
+ to && scrollPositions.has(to) ? scrollPositions.get(to)! : { x: 0, y: 0 };
132
+
133
+ return {
134
+ x: -toScroll.x + fromScroll.x,
135
+ y: -toScroll.y + fromScroll.y,
136
+ };
137
+ };
138
+
139
+ function checkAndResolve() {
140
+ if (pendingTransition?.from && pendingTransition?.to) {
141
+ const transition = findMatchingTransition(
142
+ pendingTransition.from,
143
+ pendingTransition.to,
144
+ processedTransitions
145
+ );
146
+ const result = transition || options.defaultTransition;
147
+ const scrollOffset = calculateScrollOffset();
148
+ const context = { scrollOffset };
149
+
150
+ if (result) {
151
+ if (result.out && pendingTransition.outResolve) {
152
+ pendingTransition.outResolve((element) =>
153
+ result.out!(element, context)
154
+ );
155
+ }
156
+ if (result.in && pendingTransition.inResolve) {
157
+ pendingTransition.inResolve((element) =>
158
+ result.in!(element, context)
159
+ );
160
+ }
161
+ }
162
+
163
+ pendingTransition = null;
164
+ }
165
+ }
166
+
167
+ const getTransition = async (path: string, type: "out" | "in") => {
168
+ if (type === "in") {
169
+ // If IN is called but no OUT is pending, no transition occurs (e.g., page refresh)
170
+ if (!pendingTransition || !pendingTransition.from) {
171
+ return () => ({}); // Return empty transition
172
+ }
173
+ }
174
+
175
+ if (!pendingTransition) {
176
+ pendingTransition = {};
177
+ }
178
+
179
+ if (type === "out") {
180
+ pendingTransition.from = path;
181
+ return new Promise<GetTransitionConfig>((resolve) => {
182
+ pendingTransition!.outResolve = resolve;
183
+ checkAndResolve();
184
+ });
185
+ } else {
186
+ pendingTransition.to = path;
187
+ return new Promise<GetTransitionConfig>((resolve) => {
188
+ pendingTransition!.inResolve = resolve;
189
+ checkAndResolve();
190
+ });
191
+ }
192
+ };
193
+
194
+ return (path: string) => {
195
+ return {
196
+ key: path,
197
+ in: async (element: HTMLElement) => {
198
+ // Start scroll tracking for this path when element enters
199
+ startScrollTracking(element, path);
200
+
201
+ const transitionConfig = await getTransition(path, "in");
202
+ return transitionConfig(element);
203
+ },
204
+ out: async (element: HTMLElement) => {
205
+ const transitionConfig = await getTransition(path, "out");
206
+ return transitionConfig(element);
207
+ },
208
+ };
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Matches a path against a pattern
214
+ * Supports exact matches and wildcard patterns
215
+ *
216
+ * @example
217
+ * matchPath('/products', '/products') // true
218
+ * matchPath('/products/123', '/products/*') // true
219
+ * matchPath('/products/123', '/products') // false
220
+ * matchPath('/anything', '*') // true
221
+ */
222
+ function findMatchingTransition<TContext>(
223
+ from: string,
224
+ to: string,
225
+ transitions: Array<{
226
+ from: string;
227
+ to: string;
228
+ transition: Transition<TContext>;
229
+ }>
230
+ ): Transition<TContext> | null {
231
+ // First try to find exact match for both from and to paths
232
+ for (const config of transitions) {
233
+ if (matchPath(from, config.from) && matchPath(to, config.to)) {
234
+ return config.transition;
235
+ }
236
+ }
237
+
238
+ // Then try wildcard matches if no exact match found
239
+ for (const config of transitions) {
240
+ if (
241
+ (config.from === "*" || matchPath(from, config.from)) &&
242
+ (config.to === "*" || matchPath(to, config.to))
243
+ ) {
244
+ return config.transition;
245
+ }
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ function matchPath(path: string, pattern: string): boolean {
252
+ // Universal match - asterisk matches any path
253
+ if (pattern === "*") {
254
+ return true;
255
+ }
256
+
257
+ // Wildcard match - pattern ending with /* matches path and subpaths
258
+ if (pattern.endsWith("/*")) {
259
+ const prefix = pattern.slice(0, -2);
260
+ return path === prefix || path.startsWith(prefix + "/");
261
+ }
262
+
263
+ // Exact match - paths must be identical
264
+ return path === pattern;
265
+ }
@@ -0,0 +1,246 @@
1
+ import type { Transition, TransitionCallback } from "./types";
2
+ import { Animator } from "./animator";
3
+ /**
4
+ * Creates a transition callback that can be used with framework-specific implementations
5
+ * This is the core logic that frameworks can wrap with their own APIs
6
+ *
7
+ * UX Animation Behavior - 4 Main Scenarios:
8
+ *
9
+ * 1. No animation running + IN trigger:
10
+ * - Start entrance animation (0 → 1)
11
+ * - Return cleanup function for exit
12
+ *
13
+ * 2. No animation running + OUT trigger:
14
+ * - Clone element, start exit animation (1 → 0)
15
+ * - Remove clone when complete
16
+ *
17
+ * 3. IN animation running + OUT trigger:
18
+ * - Stop current IN animation (DOM is disappearing)
19
+ * - Clone element for exit animation
20
+ * - Create REVERSED IN animation (not OUT animation) with current state
21
+ * - This gives natural backward motion instead of jumping to OUT definition
22
+ *
23
+ * 4. OUT animation running + IN trigger:
24
+ * - Stop current OUT animation (cleanup any cloned elements)
25
+ * - Create REVERSED OUT animation (not IN animation) with current state
26
+ * - This gives natural backward motion instead of jumping to IN definition
27
+ * - Switch to entrance mode
28
+ *
29
+ * Closure Structure:
30
+ * - Outer function: Returns entrance callback
31
+ * - Inner function (entrance callback): Returns cleanup callback (exit)
32
+ * - Cleanup callback: Handles exit transitions
33
+ */
34
+ export function createTransitionCallback(
35
+ getTransition: () => Transition,
36
+ options?: {
37
+ onCleanupEnd?: () => void;
38
+ }
39
+ ): TransitionCallback {
40
+ let currentAnimation: Animator | null = null;
41
+ let currentClone: HTMLElement | null = null; // Track current clone element
42
+ let parentRef: Element | null = null;
43
+ let nextSiblingRef: Element | null = null;
44
+ let isEntering = false; // Track current transition direction
45
+
46
+ const runEntrance = async (element: HTMLElement) => {
47
+ // Scenario 4: OUT animation running + IN trigger
48
+ if (currentAnimation && currentAnimation.getIsAnimating() && !isEntering) {
49
+ // Stop current OUT animation
50
+ const currentState = currentAnimation.getCurrentState();
51
+ currentAnimation.stop();
52
+
53
+ // Remove clone immediately
54
+ if (currentClone) {
55
+ currentClone.remove();
56
+ currentClone = null;
57
+ }
58
+
59
+ // Start reversed OUT animation on original element (IN direction)
60
+ isEntering = true;
61
+ const transition = getTransition();
62
+ if (!transition.out) return;
63
+
64
+ const outConfig = await Promise.resolve(transition.out(element));
65
+
66
+ // Use OUT config but reverse direction (backward)
67
+ currentAnimation = Animator.fromState(currentState, {
68
+ from: 1,
69
+ to: 0,
70
+ spring: outConfig.spring,
71
+ onStart: outConfig.onStart,
72
+ onUpdate: (value) => {
73
+ outConfig.tick?.(value);
74
+ },
75
+ onComplete: () => {
76
+ currentAnimation = null;
77
+ isEntering = false;
78
+ outConfig.onEnd?.();
79
+ },
80
+ });
81
+
82
+ currentAnimation.backward();
83
+ return;
84
+ }
85
+
86
+ // Scenario 1: No animation running OR IN already running
87
+ if (!currentAnimation || !currentAnimation.getIsAnimating()) {
88
+ // Start new IN animation
89
+ isEntering = true;
90
+ const transition = getTransition();
91
+ if (!transition.in) return;
92
+
93
+ const inConfig = await Promise.resolve(transition.in(element));
94
+
95
+ // Apply prepare function if provided
96
+ inConfig.prepare?.(element);
97
+
98
+ currentAnimation = new Animator({
99
+ from: 0,
100
+ to: 1,
101
+ spring: inConfig.spring,
102
+ onStart: inConfig.onStart,
103
+ onUpdate: (value) => {
104
+ inConfig.tick?.(value);
105
+ },
106
+ onComplete: () => {
107
+ currentAnimation = null;
108
+ isEntering = false;
109
+ inConfig.onEnd?.();
110
+ },
111
+ });
112
+
113
+ currentAnimation.forward();
114
+ }
115
+ // If IN is already running, just continue
116
+ };
117
+ function runExitTransition(element: HTMLElement) {
118
+ // Helper function to insert clone into DOM
119
+ const insertClone = () => {
120
+ if (!parentRef || !currentClone) return;
121
+
122
+ if (nextSiblingRef && parentRef.contains(nextSiblingRef)) {
123
+ parentRef.insertBefore(currentClone, nextSiblingRef);
124
+ } else {
125
+ parentRef.appendChild(currentClone);
126
+ }
127
+ };
128
+
129
+ // Helper function to handle cleanup
130
+ const cleanup = () => {
131
+ if (currentClone) {
132
+ currentClone.remove();
133
+ currentClone = null;
134
+ }
135
+ currentAnimation = null;
136
+ isEntering = false;
137
+ options?.onCleanupEnd?.();
138
+ };
139
+ // Get transition
140
+ const transition = getTransition();
141
+ // Clone the element upfront
142
+ currentClone = element.cloneNode(true) as HTMLElement;
143
+
144
+ // Scenario 3: IN animation running + OUT trigger
145
+ if (currentAnimation && currentAnimation.getIsAnimating() && isEntering) {
146
+ // Stop current IN animation and create REVERSED IN animation (not OUT)
147
+ const currentState = currentAnimation.getCurrentState();
148
+ currentAnimation.stop();
149
+ isEntering = false;
150
+
151
+ // Get the IN config (not OUT) because we want to reverse the IN animation
152
+ if (!transition.in) {
153
+ currentClone = null;
154
+ cleanup();
155
+ return;
156
+ }
157
+
158
+ Promise.resolve(transition.in(currentClone)).then(async (inConfig) => {
159
+ const outConfig =
160
+ currentClone && (await transition.out?.(currentClone));
161
+
162
+ if (outConfig?.prepare) {
163
+ currentClone && outConfig.prepare(currentClone);
164
+ }
165
+ // Insert clone only after we have the transition config
166
+ insertClone();
167
+
168
+ // Use IN config but reverse direction (backward)
169
+ currentAnimation = Animator.fromState(currentState, {
170
+ from: 0,
171
+ to: 1,
172
+ spring: inConfig.spring,
173
+ onStart: inConfig.onStart,
174
+ onUpdate: (value) => {
175
+ inConfig.tick?.(value);
176
+ },
177
+ onComplete: () => {
178
+ inConfig.onEnd?.();
179
+ cleanup();
180
+ },
181
+ });
182
+
183
+ currentAnimation.backward();
184
+ });
185
+
186
+ return;
187
+ }
188
+
189
+ // Scenario 2: No animation running OR OUT already running
190
+ if (
191
+ !currentAnimation ||
192
+ !currentAnimation.getIsAnimating() ||
193
+ !isEntering
194
+ ) {
195
+ isEntering = false;
196
+
197
+ if (!transition.out) {
198
+ currentClone = null;
199
+ cleanup();
200
+ return;
201
+ }
202
+
203
+ Promise.resolve(transition.out(currentClone)).then((outConfig) => {
204
+ // Apply prepare function if provided
205
+
206
+ currentClone && outConfig.prepare?.(currentClone);
207
+
208
+ // Insert clone after preparation
209
+ insertClone();
210
+
211
+ currentAnimation = new Animator({
212
+ from: 1,
213
+ to: 0,
214
+ spring: outConfig.spring,
215
+ onStart: outConfig.onStart,
216
+ onUpdate: (value) => {
217
+ outConfig.tick?.(value);
218
+ },
219
+ onComplete: () => {
220
+ outConfig.onEnd?.();
221
+ cleanup();
222
+ },
223
+ });
224
+
225
+ currentAnimation.forward();
226
+ });
227
+ }
228
+ // If OUT is already running, just continue
229
+ }
230
+
231
+ return (element: HTMLElement | null) => {
232
+ if (!element) return;
233
+ parentRef = element.parentElement;
234
+ nextSiblingRef = element.nextElementSibling;
235
+
236
+ const transition = getTransition();
237
+ if (transition.in) {
238
+ runEntrance(element);
239
+ }
240
+
241
+ // Return cleanup function for exit transition
242
+ return () => {
243
+ runExitTransition(element);
244
+ };
245
+ };
246
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./transition";
3
+ export * from "./create-ssgoi-transition-context";
@@ -0,0 +1,76 @@
1
+ import { createTransitionCallback } from "./create-transition-callback";
2
+ import type { Transition, TransitionCallback } from "./types";
3
+
4
+ /**
5
+ * Key type for transitions - can be string or symbol
6
+ */
7
+ type TransitionKey = string | symbol;
8
+
9
+ /**
10
+ * Centralized transition management
11
+ * Uses string/symbol keys for all storage
12
+ */
13
+
14
+ // Map to store transition definitions by key
15
+ const transitionDefinitions = new Map<TransitionKey, Transition>();
16
+
17
+ // Map to store transition callbacks by key
18
+ const transitionCallbacks = new Map<TransitionKey, TransitionCallback>();
19
+
20
+ /**
21
+ * Registers a transition with a key and returns the callback
22
+ * Usage: registerTransition('fade', { in: fadeIn, out: fadeOut })
23
+ */
24
+ function registerTransition(
25
+ key: TransitionKey,
26
+ transition: Transition
27
+ ): TransitionCallback {
28
+ transitionDefinitions.set(key, transition);
29
+
30
+ // Return existing callback if it exists
31
+ let callback = transitionCallbacks.get(key);
32
+ if (callback) {
33
+ return callback;
34
+ }
35
+
36
+ // Create new callback
37
+ callback = createTransitionCallback(
38
+ () => {
39
+ const trans = transitionDefinitions.get(key);
40
+ if (!trans) {
41
+ console.warn(`Transition "${String(key)}" not found`);
42
+ return {};
43
+ }
44
+ return trans;
45
+ },
46
+ {
47
+ onCleanupEnd: () => unregisterTransition(key),
48
+ }
49
+ );
50
+ transitionCallbacks.set(key, callback);
51
+ return callback;
52
+ }
53
+
54
+ /**
55
+ * Unregisters a transition and cleans up associated resources
56
+ */
57
+ function unregisterTransition(key: TransitionKey): void {
58
+ transitionDefinitions.delete(key);
59
+ transitionCallbacks.delete(key);
60
+ }
61
+
62
+ /**
63
+ * Framework-agnostic transition function that can be used as a ref
64
+ * Usage: <div ref={transition({ key: 'fade', in, out })} />
65
+ */
66
+ export function transition(options: {
67
+ key: TransitionKey;
68
+ in?: Transition["in"];
69
+ out?: Transition["out"];
70
+ }): TransitionCallback {
71
+ // Register transition and get callback
72
+ return registerTransition(options.key, {
73
+ in: options.in,
74
+ out: options.out,
75
+ });
76
+ }
@@ -0,0 +1,55 @@
1
+ interface BlurOptions {
2
+ amount?: number;
3
+ scale?: boolean;
4
+ fade?: boolean;
5
+ spring?: {
6
+ stiffness?: number;
7
+ damping?: number;
8
+ };
9
+ }
10
+
11
+ export const blur = (options: BlurOptions = {}) => {
12
+ const {
13
+ amount = 10,
14
+ scale = false,
15
+ fade = true,
16
+ spring = { stiffness: 300, damping: 30 }
17
+ } = options;
18
+
19
+ return {
20
+ in: (element: HTMLElement) => ({
21
+ spring,
22
+ tick: (progress: number) => {
23
+ const blurValue = (1 - progress) * amount;
24
+ const filters = [`blur(${blurValue}px)`];
25
+
26
+ element.style.filter = filters.join(' ');
27
+
28
+ if (scale) {
29
+ element.style.transform = `scale(${0.8 + progress * 0.2})`;
30
+ }
31
+
32
+ if (fade) {
33
+ element.style.opacity = progress.toString();
34
+ }
35
+ }
36
+ }),
37
+ out: (element: HTMLElement) => ({
38
+ spring,
39
+ tick: (progress: number) => {
40
+ const blurValue = (1 - progress) * amount;
41
+ const filters = [`blur(${blurValue}px)`];
42
+
43
+ element.style.filter = filters.join(' ');
44
+
45
+ if (scale) {
46
+ element.style.transform = `scale(${0.8 + progress * 0.2})`;
47
+ }
48
+
49
+ if (fade) {
50
+ element.style.opacity = progress.toString();
51
+ }
52
+ }
53
+ })
54
+ };
55
+ };
@@ -0,0 +1,61 @@
1
+ interface BounceOptions {
2
+ height?: number;
3
+ scale?: boolean;
4
+ fade?: boolean;
5
+ spring?: {
6
+ stiffness?: number;
7
+ damping?: number;
8
+ };
9
+ }
10
+
11
+ export const bounce = (options: BounceOptions = {}) => {
12
+ const {
13
+ height = 20,
14
+ scale = true,
15
+ fade = false,
16
+ spring = { stiffness: 800, damping: 15 }
17
+ } = options;
18
+
19
+ return {
20
+ in: (element: HTMLElement) => ({
21
+ spring,
22
+ tick: (progress: number) => {
23
+ const transforms = [];
24
+
25
+ // Bounce effect using sine wave
26
+ const bounceOffset = Math.sin(progress * Math.PI) * height * (1 - progress);
27
+ transforms.push(`translateY(${-bounceOffset}px)`);
28
+
29
+ if (scale) {
30
+ transforms.push(`scale(${0.8 + progress * 0.2})`);
31
+ }
32
+
33
+ element.style.transform = transforms.join(' ');
34
+
35
+ if (fade) {
36
+ element.style.opacity = progress.toString();
37
+ }
38
+ }
39
+ }),
40
+ out: (element: HTMLElement) => ({
41
+ spring,
42
+ tick: (progress: number) => {
43
+ const transforms = [];
44
+
45
+ // Bounce effect using sine wave
46
+ const bounceOffset = Math.sin(progress * Math.PI) * height * (1 - progress);
47
+ transforms.push(`translateY(${-bounceOffset}px)`);
48
+
49
+ if (scale) {
50
+ transforms.push(`scale(${0.8 + progress * 0.2})`);
51
+ }
52
+
53
+ element.style.transform = transforms.join(' ');
54
+
55
+ if (fade) {
56
+ element.style.opacity = progress.toString();
57
+ }
58
+ }
59
+ })
60
+ };
61
+ };