@hua-labs/motion-core 2.2.2 → 2.3.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 +28 -111
- package/dist/index.d.mts +669 -131
- package/dist/index.mjs +2307 -712
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
- package/dist/index.d.ts +0 -1089
- package/dist/index.js +0 -4248
- package/dist/index.js.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import { createContext, useState, useRef, useCallback, useEffect, useMemo, createElement, useContext } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
4
|
|
|
4
5
|
// src/core/MotionEngine.ts
|
|
5
6
|
var MotionEngine = class {
|
|
@@ -194,246 +195,202 @@ var TransitionEffects = class _TransitionEffects {
|
|
|
194
195
|
return _TransitionEffects.instance;
|
|
195
196
|
}
|
|
196
197
|
/**
|
|
197
|
-
*
|
|
198
|
+
* 공통 전환 실행 헬퍼
|
|
198
199
|
*/
|
|
199
|
-
async
|
|
200
|
+
async executeTransition(element, options, config) {
|
|
200
201
|
const transitionId = this.generateTransitionId();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
config.setup();
|
|
203
|
+
this.enableGPUAcceleration(element);
|
|
204
|
+
let resolveTransition;
|
|
205
|
+
const completed = new Promise((resolve) => {
|
|
206
|
+
resolveTransition = resolve;
|
|
207
|
+
});
|
|
208
|
+
const motionId = await motionEngine.motion(
|
|
209
|
+
element,
|
|
210
|
+
[
|
|
211
|
+
{ progress: 0, properties: config.keyframes[0] },
|
|
212
|
+
{ progress: 1, properties: config.keyframes[1] }
|
|
213
|
+
],
|
|
214
|
+
{
|
|
215
|
+
duration: options.duration,
|
|
216
|
+
easing: options.easing || this.getDefaultEasing(),
|
|
217
|
+
delay: options.delay,
|
|
218
|
+
onStart: options.onTransitionStart,
|
|
219
|
+
onUpdate: config.onUpdate,
|
|
220
|
+
onComplete: () => {
|
|
221
|
+
config.onCleanup();
|
|
222
|
+
options.onTransitionComplete?.();
|
|
223
|
+
this.activeTransitions.delete(transitionId);
|
|
224
|
+
resolveTransition();
|
|
225
|
+
}
|
|
208
226
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
onComplete: () => {
|
|
226
|
-
options.onTransitionComplete?.();
|
|
227
|
-
this.activeTransitions.delete(transitionId);
|
|
228
|
-
resolve();
|
|
229
|
-
}
|
|
227
|
+
);
|
|
228
|
+
this.activeTransitions.set(transitionId, motionId);
|
|
229
|
+
return completed;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 페이드 인/아웃 전환
|
|
233
|
+
*/
|
|
234
|
+
async fade(element, options) {
|
|
235
|
+
const initialOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
|
|
236
|
+
const targetOpacity = options.direction === "reverse" ? 0 : 1;
|
|
237
|
+
return this.executeTransition(element, options, {
|
|
238
|
+
setup: () => {
|
|
239
|
+
if (options.direction === "reverse") {
|
|
240
|
+
element.style.opacity = initialOpacity.toString();
|
|
241
|
+
} else {
|
|
242
|
+
element.style.opacity = "0";
|
|
230
243
|
}
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
},
|
|
245
|
+
keyframes: [
|
|
246
|
+
{ opacity: options.direction === "reverse" ? initialOpacity : 0 },
|
|
247
|
+
{ opacity: targetOpacity }
|
|
248
|
+
],
|
|
249
|
+
onUpdate: (progress) => {
|
|
250
|
+
const currentOpacity = options.direction === "reverse" ? initialOpacity * (1 - progress) : targetOpacity * progress;
|
|
251
|
+
element.style.opacity = currentOpacity.toString();
|
|
252
|
+
},
|
|
253
|
+
onCleanup: () => {
|
|
254
|
+
}
|
|
233
255
|
});
|
|
234
256
|
}
|
|
235
257
|
/**
|
|
236
258
|
* 슬라이드 전환
|
|
237
259
|
*/
|
|
238
260
|
async slide(element, options) {
|
|
239
|
-
const transitionId = this.generateTransitionId();
|
|
240
261
|
const distance = options.distance || 100;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.enableGPUAcceleration(element);
|
|
248
|
-
const motionId = await motionEngine.motion(
|
|
249
|
-
element,
|
|
250
|
-
[
|
|
251
|
-
{ progress: 0, properties: { translateX: isReverse ? 0 : distance } },
|
|
252
|
-
{ progress: 1, properties: { translateX: isReverse ? distance : 0 } }
|
|
253
|
-
],
|
|
254
|
-
{
|
|
255
|
-
duration: options.duration,
|
|
256
|
-
easing: options.easing || this.getDefaultEasing(),
|
|
257
|
-
delay: options.delay,
|
|
258
|
-
onStart: options.onTransitionStart,
|
|
259
|
-
onUpdate: (progress) => {
|
|
260
|
-
const currentTranslateX = isReverse ? distance * progress : distance * (1 - progress);
|
|
261
|
-
element.style.transform = `translateX(${currentTranslateX}px)`;
|
|
262
|
-
},
|
|
263
|
-
onComplete: () => {
|
|
264
|
-
element.style.transform = initialTransform;
|
|
265
|
-
options.onTransitionComplete?.();
|
|
266
|
-
this.activeTransitions.delete(transitionId);
|
|
267
|
-
resolve();
|
|
268
|
-
}
|
|
262
|
+
const initialTransform = getComputedStyle(element).transform;
|
|
263
|
+
const isReverse = options.direction === "reverse";
|
|
264
|
+
return this.executeTransition(element, options, {
|
|
265
|
+
setup: () => {
|
|
266
|
+
if (!isReverse) {
|
|
267
|
+
element.style.transform = `translateX(${distance}px)`;
|
|
269
268
|
}
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
},
|
|
270
|
+
keyframes: [
|
|
271
|
+
{ translateX: isReverse ? 0 : distance },
|
|
272
|
+
{ translateX: isReverse ? distance : 0 }
|
|
273
|
+
],
|
|
274
|
+
onUpdate: (progress) => {
|
|
275
|
+
const currentTranslateX = isReverse ? distance * progress : distance * (1 - progress);
|
|
276
|
+
element.style.transform = `translateX(${currentTranslateX}px)`;
|
|
277
|
+
},
|
|
278
|
+
onCleanup: () => {
|
|
279
|
+
element.style.transform = initialTransform;
|
|
280
|
+
}
|
|
272
281
|
});
|
|
273
282
|
}
|
|
274
283
|
/**
|
|
275
284
|
* 스케일 전환
|
|
276
285
|
*/
|
|
277
286
|
async scale(element, options) {
|
|
278
|
-
const transitionId = this.generateTransitionId();
|
|
279
287
|
const scaleValue = options.scale || 0.8;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.enableGPUAcceleration(element);
|
|
287
|
-
const motionId = await motionEngine.motion(
|
|
288
|
-
element,
|
|
289
|
-
[
|
|
290
|
-
{ progress: 0, properties: { scale: isReverse ? 1 : scaleValue } },
|
|
291
|
-
{ progress: 1, properties: { scale: isReverse ? scaleValue : 1 } }
|
|
292
|
-
],
|
|
293
|
-
{
|
|
294
|
-
duration: options.duration,
|
|
295
|
-
easing: options.easing || this.getDefaultEasing(),
|
|
296
|
-
delay: options.delay,
|
|
297
|
-
onStart: options.onTransitionStart,
|
|
298
|
-
onUpdate: (progress) => {
|
|
299
|
-
const currentScale = isReverse ? 1 - (1 - scaleValue) * progress : scaleValue + (1 - scaleValue) * progress;
|
|
300
|
-
element.style.transform = `scale(${currentScale})`;
|
|
301
|
-
},
|
|
302
|
-
onComplete: () => {
|
|
303
|
-
element.style.transform = initialTransform;
|
|
304
|
-
options.onTransitionComplete?.();
|
|
305
|
-
this.activeTransitions.delete(transitionId);
|
|
306
|
-
resolve();
|
|
307
|
-
}
|
|
288
|
+
const initialTransform = getComputedStyle(element).transform;
|
|
289
|
+
const isReverse = options.direction === "reverse";
|
|
290
|
+
return this.executeTransition(element, options, {
|
|
291
|
+
setup: () => {
|
|
292
|
+
if (!isReverse) {
|
|
293
|
+
element.style.transform = `scale(${scaleValue})`;
|
|
308
294
|
}
|
|
309
|
-
|
|
310
|
-
|
|
295
|
+
},
|
|
296
|
+
keyframes: [
|
|
297
|
+
{ scale: isReverse ? 1 : scaleValue },
|
|
298
|
+
{ scale: isReverse ? scaleValue : 1 }
|
|
299
|
+
],
|
|
300
|
+
onUpdate: (progress) => {
|
|
301
|
+
const currentScale = isReverse ? 1 - (1 - scaleValue) * progress : scaleValue + (1 - scaleValue) * progress;
|
|
302
|
+
element.style.transform = `scale(${currentScale})`;
|
|
303
|
+
},
|
|
304
|
+
onCleanup: () => {
|
|
305
|
+
element.style.transform = initialTransform;
|
|
306
|
+
}
|
|
311
307
|
});
|
|
312
308
|
}
|
|
313
309
|
/**
|
|
314
310
|
* 플립 전환 (3D 회전)
|
|
315
311
|
*/
|
|
316
312
|
async flip(element, options) {
|
|
317
|
-
const transitionId = this.generateTransitionId();
|
|
318
313
|
const perspective = options.perspective || 1e3;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.enableGPUAcceleration(element);
|
|
328
|
-
const motionId = await motionEngine.motion(
|
|
329
|
-
element,
|
|
330
|
-
[
|
|
331
|
-
{ progress: 0, properties: { rotateY: isReverse ? 0 : 90 } },
|
|
332
|
-
{ progress: 1, properties: { rotateY: isReverse ? 90 : 0 } }
|
|
333
|
-
],
|
|
334
|
-
{
|
|
335
|
-
duration: options.duration,
|
|
336
|
-
easing: options.easing || this.getDefaultEasing(),
|
|
337
|
-
delay: options.delay,
|
|
338
|
-
onStart: options.onTransitionStart,
|
|
339
|
-
onUpdate: (progress) => {
|
|
340
|
-
const currentRotateY = isReverse ? 90 * progress : 90 * (1 - progress);
|
|
341
|
-
element.style.transform = `rotateY(${currentRotateY}deg)`;
|
|
342
|
-
},
|
|
343
|
-
onComplete: () => {
|
|
344
|
-
element.style.transform = initialTransform;
|
|
345
|
-
element.style.perspective = "";
|
|
346
|
-
element.style.transformStyle = "";
|
|
347
|
-
options.onTransitionComplete?.();
|
|
348
|
-
this.activeTransitions.delete(transitionId);
|
|
349
|
-
resolve();
|
|
350
|
-
}
|
|
314
|
+
const initialTransform = getComputedStyle(element).transform;
|
|
315
|
+
const isReverse = options.direction === "reverse";
|
|
316
|
+
return this.executeTransition(element, options, {
|
|
317
|
+
setup: () => {
|
|
318
|
+
element.style.perspective = `${perspective}px`;
|
|
319
|
+
element.style.transformStyle = "preserve-3d";
|
|
320
|
+
if (!isReverse) {
|
|
321
|
+
element.style.transform = `rotateY(90deg)`;
|
|
351
322
|
}
|
|
352
|
-
|
|
353
|
-
|
|
323
|
+
},
|
|
324
|
+
keyframes: [
|
|
325
|
+
{ rotateY: isReverse ? 0 : 90 },
|
|
326
|
+
{ rotateY: isReverse ? 90 : 0 }
|
|
327
|
+
],
|
|
328
|
+
onUpdate: (progress) => {
|
|
329
|
+
const currentRotateY = isReverse ? 90 * progress : 90 * (1 - progress);
|
|
330
|
+
element.style.transform = `rotateY(${currentRotateY}deg)`;
|
|
331
|
+
},
|
|
332
|
+
onCleanup: () => {
|
|
333
|
+
element.style.transform = initialTransform;
|
|
334
|
+
element.style.perspective = "";
|
|
335
|
+
element.style.transformStyle = "";
|
|
336
|
+
}
|
|
354
337
|
});
|
|
355
338
|
}
|
|
356
339
|
/**
|
|
357
340
|
* 큐브 전환 (3D 큐브 회전)
|
|
358
341
|
*/
|
|
359
342
|
async cube(element, options) {
|
|
360
|
-
const transitionId = this.generateTransitionId();
|
|
361
343
|
const perspective = options.perspective || 1200;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
this.enableGPUAcceleration(element);
|
|
371
|
-
const motionId = await motionEngine.motion(
|
|
372
|
-
element,
|
|
373
|
-
[
|
|
374
|
-
{ progress: 0, properties: { rotateX: isReverse ? 0 : 90, rotateY: isReverse ? 0 : 45 } },
|
|
375
|
-
{ progress: 1, properties: { rotateX: isReverse ? 90 : 0, rotateY: isReverse ? 45 : 0 } }
|
|
376
|
-
],
|
|
377
|
-
{
|
|
378
|
-
duration: options.duration,
|
|
379
|
-
easing: options.easing || this.getDefaultEasing(),
|
|
380
|
-
delay: options.delay,
|
|
381
|
-
onStart: options.onTransitionStart,
|
|
382
|
-
onUpdate: (progress) => {
|
|
383
|
-
const currentRotateX = isReverse ? 90 * progress : 90 * (1 - progress);
|
|
384
|
-
const currentRotateY = isReverse ? 45 * progress : 45 * (1 - progress);
|
|
385
|
-
element.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
|
|
386
|
-
},
|
|
387
|
-
onComplete: () => {
|
|
388
|
-
element.style.transform = initialTransform;
|
|
389
|
-
element.style.perspective = "";
|
|
390
|
-
element.style.transformStyle = "";
|
|
391
|
-
options.onTransitionComplete?.();
|
|
392
|
-
this.activeTransitions.delete(transitionId);
|
|
393
|
-
resolve();
|
|
394
|
-
}
|
|
344
|
+
const initialTransform = getComputedStyle(element).transform;
|
|
345
|
+
const isReverse = options.direction === "reverse";
|
|
346
|
+
return this.executeTransition(element, options, {
|
|
347
|
+
setup: () => {
|
|
348
|
+
element.style.perspective = `${perspective}px`;
|
|
349
|
+
element.style.transformStyle = "preserve-3d";
|
|
350
|
+
if (!isReverse) {
|
|
351
|
+
element.style.transform = `rotateX(90deg) rotateY(45deg)`;
|
|
395
352
|
}
|
|
396
|
-
|
|
397
|
-
|
|
353
|
+
},
|
|
354
|
+
keyframes: [
|
|
355
|
+
{ rotateX: isReverse ? 0 : 90, rotateY: isReverse ? 0 : 45 },
|
|
356
|
+
{ rotateX: isReverse ? 90 : 0, rotateY: isReverse ? 45 : 0 }
|
|
357
|
+
],
|
|
358
|
+
onUpdate: (progress) => {
|
|
359
|
+
const currentRotateX = isReverse ? 90 * progress : 90 * (1 - progress);
|
|
360
|
+
const currentRotateY = isReverse ? 45 * progress : 45 * (1 - progress);
|
|
361
|
+
element.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
|
|
362
|
+
},
|
|
363
|
+
onCleanup: () => {
|
|
364
|
+
element.style.transform = initialTransform;
|
|
365
|
+
element.style.perspective = "";
|
|
366
|
+
element.style.transformStyle = "";
|
|
367
|
+
}
|
|
398
368
|
});
|
|
399
369
|
}
|
|
400
370
|
/**
|
|
401
371
|
* 모프 전환 (복합 변형)
|
|
402
372
|
*/
|
|
403
373
|
async morph(element, options) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
this.enableGPUAcceleration(element);
|
|
412
|
-
const motionId = await motionEngine.motion(
|
|
413
|
-
element,
|
|
414
|
-
[
|
|
415
|
-
{ progress: 0, properties: { scale: isReverse ? 1 : 0.9, rotate: isReverse ? 0 : 5 } },
|
|
416
|
-
{ progress: 1, properties: { scale: isReverse ? 0.9 : 1, rotate: isReverse ? 5 : 0 } }
|
|
417
|
-
],
|
|
418
|
-
{
|
|
419
|
-
duration: options.duration,
|
|
420
|
-
easing: options.easing || this.getDefaultEasing(),
|
|
421
|
-
delay: options.delay,
|
|
422
|
-
onStart: options.onTransitionStart,
|
|
423
|
-
onUpdate: (progress) => {
|
|
424
|
-
const currentScale = isReverse ? 1 - 0.1 * progress : 0.9 + 0.1 * progress;
|
|
425
|
-
const currentRotate = isReverse ? 5 * progress : 5 * (1 - progress);
|
|
426
|
-
element.style.transform = `scale(${currentScale}) rotate(${currentRotate}deg)`;
|
|
427
|
-
},
|
|
428
|
-
onComplete: () => {
|
|
429
|
-
element.style.transform = initialTransform;
|
|
430
|
-
options.onTransitionComplete?.();
|
|
431
|
-
this.activeTransitions.delete(transitionId);
|
|
432
|
-
resolve();
|
|
433
|
-
}
|
|
374
|
+
const initialTransform = getComputedStyle(element).transform;
|
|
375
|
+
const isReverse = options.direction === "reverse";
|
|
376
|
+
return this.executeTransition(element, options, {
|
|
377
|
+
setup: () => {
|
|
378
|
+
if (!isReverse) {
|
|
379
|
+
element.style.transform = `scale(0.9) rotate(5deg)`;
|
|
434
380
|
}
|
|
435
|
-
|
|
436
|
-
|
|
381
|
+
},
|
|
382
|
+
keyframes: [
|
|
383
|
+
{ scale: isReverse ? 1 : 0.9, rotate: isReverse ? 0 : 5 },
|
|
384
|
+
{ scale: isReverse ? 0.9 : 1, rotate: isReverse ? 5 : 0 }
|
|
385
|
+
],
|
|
386
|
+
onUpdate: (progress) => {
|
|
387
|
+
const currentScale = isReverse ? 1 - 0.1 * progress : 0.9 + 0.1 * progress;
|
|
388
|
+
const currentRotate = isReverse ? 5 * progress : 5 * (1 - progress);
|
|
389
|
+
element.style.transform = `scale(${currentScale}) rotate(${currentRotate}deg)`;
|
|
390
|
+
},
|
|
391
|
+
onCleanup: () => {
|
|
392
|
+
element.style.transform = initialTransform;
|
|
393
|
+
}
|
|
437
394
|
});
|
|
438
395
|
}
|
|
439
396
|
/**
|
|
@@ -496,305 +453,6 @@ var TransitionEffects = class _TransitionEffects {
|
|
|
496
453
|
};
|
|
497
454
|
var transitionEffects = TransitionEffects.getInstance();
|
|
498
455
|
|
|
499
|
-
// src/core/PerformanceOptimizer.ts
|
|
500
|
-
var PerformanceOptimizer = class _PerformanceOptimizer {
|
|
501
|
-
constructor() {
|
|
502
|
-
this.performanceObserver = null;
|
|
503
|
-
this.layerRegistry = /* @__PURE__ */ new Set();
|
|
504
|
-
this.isMonitoring = false;
|
|
505
|
-
this.config = {
|
|
506
|
-
enableGPUAcceleration: true,
|
|
507
|
-
enableLayerSeparation: true,
|
|
508
|
-
enableMemoryOptimization: true,
|
|
509
|
-
targetFPS: 60,
|
|
510
|
-
maxLayerCount: 100,
|
|
511
|
-
memoryThreshold: 50 * 1024 * 1024
|
|
512
|
-
// 50MB
|
|
513
|
-
};
|
|
514
|
-
this.metrics = {
|
|
515
|
-
fps: 0,
|
|
516
|
-
layerCount: 0,
|
|
517
|
-
activeMotions: 0
|
|
518
|
-
};
|
|
519
|
-
this.initializePerformanceMonitoring();
|
|
520
|
-
}
|
|
521
|
-
static getInstance() {
|
|
522
|
-
if (!_PerformanceOptimizer.instance) {
|
|
523
|
-
_PerformanceOptimizer.instance = new _PerformanceOptimizer();
|
|
524
|
-
}
|
|
525
|
-
return _PerformanceOptimizer.instance;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* 성능 모니터링 초기화
|
|
529
|
-
*/
|
|
530
|
-
initializePerformanceMonitoring() {
|
|
531
|
-
if (typeof PerformanceObserver !== "undefined") {
|
|
532
|
-
try {
|
|
533
|
-
this.performanceObserver = new PerformanceObserver((list) => {
|
|
534
|
-
const entries = list.getEntries();
|
|
535
|
-
this.updatePerformanceMetrics(entries);
|
|
536
|
-
});
|
|
537
|
-
this.performanceObserver.observe({ entryTypes: ["measure", "navigation"] });
|
|
538
|
-
} catch (error) {
|
|
539
|
-
if (process.env.NODE_ENV === "development") {
|
|
540
|
-
console.warn("Performance monitoring not supported:", error);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* 성능 메트릭 업데이트
|
|
547
|
-
*/
|
|
548
|
-
updatePerformanceMetrics(entries) {
|
|
549
|
-
entries.forEach((entry) => {
|
|
550
|
-
if (entry.entryType === "measure") {
|
|
551
|
-
this.calculateFPS();
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* FPS 계산
|
|
557
|
-
*/
|
|
558
|
-
calculateFPS() {
|
|
559
|
-
const now = performance.now();
|
|
560
|
-
const deltaTime = now - (this.lastFrameTime || now);
|
|
561
|
-
this.lastFrameTime = now;
|
|
562
|
-
if (deltaTime > 0) {
|
|
563
|
-
this.metrics.fps = Math.round(1e3 / deltaTime);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* GPU 가속 활성화
|
|
568
|
-
*/
|
|
569
|
-
enableGPUAcceleration(element) {
|
|
570
|
-
if (!this.config.enableGPUAcceleration) return;
|
|
571
|
-
try {
|
|
572
|
-
element.style.willChange = "transform, opacity";
|
|
573
|
-
element.style.transform = "translateZ(0)";
|
|
574
|
-
element.style.backfaceVisibility = "hidden";
|
|
575
|
-
element.style.transformStyle = "preserve-3d";
|
|
576
|
-
this.registerLayer(element);
|
|
577
|
-
} catch (error) {
|
|
578
|
-
if (process.env.NODE_ENV === "development") {
|
|
579
|
-
console.warn("GPU acceleration failed:", error);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* 레이어 분리 및 최적화
|
|
585
|
-
*/
|
|
586
|
-
createOptimizedLayer(element) {
|
|
587
|
-
if (!this.config.enableLayerSeparation) return;
|
|
588
|
-
try {
|
|
589
|
-
element.style.transform = "translateZ(0)";
|
|
590
|
-
element.style.backfaceVisibility = "hidden";
|
|
591
|
-
element.style.perspective = "1000px";
|
|
592
|
-
this.registerLayer(element);
|
|
593
|
-
this.checkLayerLimit();
|
|
594
|
-
} catch (error) {
|
|
595
|
-
if (process.env.NODE_ENV === "development") {
|
|
596
|
-
console.warn("Layer optimization failed:", error);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* 레이어 등록
|
|
602
|
-
*/
|
|
603
|
-
registerLayer(element) {
|
|
604
|
-
if (this.layerRegistry.has(element)) return;
|
|
605
|
-
this.layerRegistry.add(element);
|
|
606
|
-
this.metrics.layerCount = this.layerRegistry.size;
|
|
607
|
-
this.checkMemoryUsage();
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* 레이어 제거
|
|
611
|
-
*/
|
|
612
|
-
removeLayer(element) {
|
|
613
|
-
if (this.layerRegistry.has(element)) {
|
|
614
|
-
this.layerRegistry.delete(element);
|
|
615
|
-
this.metrics.layerCount = this.layerRegistry.size;
|
|
616
|
-
element.style.willChange = "auto";
|
|
617
|
-
element.style.transform = "";
|
|
618
|
-
element.style.backfaceVisibility = "";
|
|
619
|
-
element.style.transformStyle = "";
|
|
620
|
-
element.style.perspective = "";
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* 레이어 수 제한 체크
|
|
625
|
-
*/
|
|
626
|
-
checkLayerLimit() {
|
|
627
|
-
if (this.metrics.layerCount > this.config.maxLayerCount) {
|
|
628
|
-
if (process.env.NODE_ENV === "development") {
|
|
629
|
-
console.warn(`Layer count (${this.metrics.layerCount}) exceeds limit (${this.config.maxLayerCount})`);
|
|
630
|
-
}
|
|
631
|
-
this.cleanupOldLayers();
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* 오래된 레이어 정리
|
|
636
|
-
*/
|
|
637
|
-
cleanupOldLayers() {
|
|
638
|
-
const layersToRemove = Array.from(this.layerRegistry).slice(0, 10);
|
|
639
|
-
layersToRemove.forEach((layer) => {
|
|
640
|
-
this.removeLayer(layer);
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* 메모리 사용량 체크
|
|
645
|
-
*/
|
|
646
|
-
checkMemoryUsage() {
|
|
647
|
-
if (!this.config.enableMemoryOptimization) return;
|
|
648
|
-
if ("memory" in performance) {
|
|
649
|
-
const memory = performance.memory;
|
|
650
|
-
this.metrics.memoryUsage = memory.usedJSHeapSize;
|
|
651
|
-
if (memory.usedJSHeapSize > this.config.memoryThreshold) {
|
|
652
|
-
if (process.env.NODE_ENV === "development") {
|
|
653
|
-
console.warn("Memory usage high, cleaning up...");
|
|
654
|
-
}
|
|
655
|
-
this.cleanupMemory();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* 메모리 정리
|
|
661
|
-
*/
|
|
662
|
-
cleanupMemory() {
|
|
663
|
-
if ("gc" in window) {
|
|
664
|
-
try {
|
|
665
|
-
window.gc();
|
|
666
|
-
} catch (error) {
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
this.cleanupOldLayers();
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* 성능 최적화 설정 업데이트
|
|
673
|
-
*/
|
|
674
|
-
updateConfig(newConfig) {
|
|
675
|
-
this.config = { ...this.config, ...newConfig };
|
|
676
|
-
if (!this.config.enableGPUAcceleration) {
|
|
677
|
-
this.disableAllGPUAcceleration();
|
|
678
|
-
}
|
|
679
|
-
if (!this.config.enableLayerSeparation) {
|
|
680
|
-
this.disableAllLayers();
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* 모든 GPU 가속 비활성화
|
|
685
|
-
*/
|
|
686
|
-
disableAllGPUAcceleration() {
|
|
687
|
-
this.layerRegistry.forEach((element) => {
|
|
688
|
-
element.style.willChange = "auto";
|
|
689
|
-
element.style.transform = "";
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* 모든 레이어 비활성화
|
|
694
|
-
*/
|
|
695
|
-
disableAllLayers() {
|
|
696
|
-
this.layerRegistry.forEach((element) => {
|
|
697
|
-
this.removeLayer(element);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* 성능 메트릭 가져오기
|
|
702
|
-
*/
|
|
703
|
-
getMetrics() {
|
|
704
|
-
return { ...this.metrics };
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* 성능 모니터링 시작
|
|
708
|
-
*/
|
|
709
|
-
startMonitoring() {
|
|
710
|
-
if (this.isMonitoring) return;
|
|
711
|
-
this.isMonitoring = true;
|
|
712
|
-
this.monitoringInterval = setInterval(() => {
|
|
713
|
-
this.updateMetrics();
|
|
714
|
-
}, 1e3);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* 성능 모니터링 중지
|
|
718
|
-
*/
|
|
719
|
-
stopMonitoring() {
|
|
720
|
-
if (!this.isMonitoring) return;
|
|
721
|
-
this.isMonitoring = false;
|
|
722
|
-
if (this.monitoringInterval) {
|
|
723
|
-
clearInterval(this.monitoringInterval);
|
|
724
|
-
this.monitoringInterval = void 0;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* 메트릭 업데이트
|
|
729
|
-
*/
|
|
730
|
-
updateMetrics() {
|
|
731
|
-
if ("memory" in performance) {
|
|
732
|
-
const memory = performance.memory;
|
|
733
|
-
this.metrics.memoryUsage = memory.usedJSHeapSize;
|
|
734
|
-
}
|
|
735
|
-
this.metrics.layerCount = this.layerRegistry.size;
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* 성능 리포트 생성
|
|
739
|
-
*/
|
|
740
|
-
generateReport() {
|
|
741
|
-
const metrics = this.getMetrics();
|
|
742
|
-
return `
|
|
743
|
-
=== HUA Motion Performance Report ===
|
|
744
|
-
FPS: ${metrics.fps}
|
|
745
|
-
Active Layers: ${metrics.layerCount}
|
|
746
|
-
Memory Usage: ${this.formatBytes(metrics.memoryUsage || 0)}
|
|
747
|
-
Active Motions: ${metrics.activeMotions}
|
|
748
|
-
GPU Acceleration: ${this.config.enableGPUAcceleration ? "Enabled" : "Disabled"}
|
|
749
|
-
Layer Separation: ${this.config.enableLayerSeparation ? "Enabled" : "Disabled"}
|
|
750
|
-
Memory Optimization: ${this.config.enableMemoryOptimization ? "Enabled" : "Disabled"}
|
|
751
|
-
=====================================
|
|
752
|
-
`.trim();
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* 바이트 단위 포맷팅
|
|
756
|
-
*/
|
|
757
|
-
formatBytes(bytes) {
|
|
758
|
-
if (bytes === 0) return "0 B";
|
|
759
|
-
const k = 1024;
|
|
760
|
-
const sizes = ["B", "KB", "MB", "GB"];
|
|
761
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
762
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* 성능 최적화 권장사항
|
|
766
|
-
*/
|
|
767
|
-
getOptimizationRecommendations() {
|
|
768
|
-
const recommendations = [];
|
|
769
|
-
const metrics = this.getMetrics();
|
|
770
|
-
if (metrics.fps < this.config.targetFPS) {
|
|
771
|
-
recommendations.push("FPS\uAC00 \uB0AE\uC2B5\uB2C8\uB2E4. GPU \uAC00\uC18D\uC744 \uD65C\uC131\uD654\uD558\uAC70\uB098 \uB808\uC774\uC5B4 \uC218\uB97C \uC904\uC774\uC138\uC694.");
|
|
772
|
-
}
|
|
773
|
-
if (metrics.layerCount > this.config.maxLayerCount * 0.8) {
|
|
774
|
-
recommendations.push("\uB808\uC774\uC5B4 \uC218\uAC00 \uB9CE\uC2B5\uB2C8\uB2E4. \uBD88\uD544\uC694\uD55C \uB808\uC774\uC5B4\uB97C \uC815\uB9AC\uD558\uC138\uC694.");
|
|
775
|
-
}
|
|
776
|
-
if (metrics.memoryUsage && metrics.memoryUsage > this.config.memoryThreshold * 0.8) {
|
|
777
|
-
recommendations.push("\uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9\uC774 \uB192\uC2B5\uB2C8\uB2E4. \uBA54\uBAA8\uB9AC \uC815\uB9AC\uB97C \uACE0\uB824\uD558\uC138\uC694.");
|
|
778
|
-
}
|
|
779
|
-
return recommendations;
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* 정리
|
|
783
|
-
*/
|
|
784
|
-
destroy() {
|
|
785
|
-
this.stopMonitoring();
|
|
786
|
-
if (this.performanceObserver) {
|
|
787
|
-
this.performanceObserver.disconnect();
|
|
788
|
-
this.performanceObserver = null;
|
|
789
|
-
}
|
|
790
|
-
this.layerRegistry.forEach((element) => {
|
|
791
|
-
this.removeLayer(element);
|
|
792
|
-
});
|
|
793
|
-
this.layerRegistry.clear();
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
var performanceOptimizer = PerformanceOptimizer.getInstance();
|
|
797
|
-
|
|
798
456
|
// src/presets/index.ts
|
|
799
457
|
var MOTION_PRESETS = {
|
|
800
458
|
hero: {
|
|
@@ -906,10 +564,10 @@ function useSimplePageMotions(config) {
|
|
|
906
564
|
const calculateMotionValues = useCallback((isVisible, elementConfig) => {
|
|
907
565
|
const preset = getMotionPreset(elementConfig.type);
|
|
908
566
|
mergeWithPreset(preset, elementConfig);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
567
|
+
const opacity = isVisible ? 1 : 0;
|
|
568
|
+
const translateY = isVisible ? 0 : 20;
|
|
569
|
+
const translateX = 0;
|
|
570
|
+
const scale = isVisible ? 1 : 0.95;
|
|
913
571
|
return { opacity, translateY, translateX, scale };
|
|
914
572
|
}, []);
|
|
915
573
|
useEffect(() => {
|
|
@@ -1169,7 +827,7 @@ function usePageMotions(config) {
|
|
|
1169
827
|
const mergedConfig = mergeWithPreset(preset, elementConfig);
|
|
1170
828
|
let opacity = state.finalVisibility ? 1 : 0;
|
|
1171
829
|
let translateY = state.finalVisibility ? 0 : 20;
|
|
1172
|
-
|
|
830
|
+
const translateX = 0;
|
|
1173
831
|
let scale = state.finalVisibility ? 1 : 0.95;
|
|
1174
832
|
if (mergedConfig.hover && state.isHovered) {
|
|
1175
833
|
scale *= 1.1;
|
|
@@ -1641,52 +1299,343 @@ function useSmartMotion(options = {}) {
|
|
|
1641
1299
|
isClicked: state.isClicked
|
|
1642
1300
|
};
|
|
1643
1301
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1302
|
+
|
|
1303
|
+
// src/profiles/neutral.ts
|
|
1304
|
+
var neutral = {
|
|
1305
|
+
name: "neutral",
|
|
1306
|
+
base: {
|
|
1307
|
+
duration: 700,
|
|
1308
|
+
easing: "ease-out",
|
|
1309
|
+
threshold: 0.1,
|
|
1310
|
+
triggerOnce: true
|
|
1311
|
+
},
|
|
1312
|
+
entrance: {
|
|
1313
|
+
slide: {
|
|
1314
|
+
distance: 32,
|
|
1315
|
+
easing: "ease-out"
|
|
1316
|
+
},
|
|
1317
|
+
fade: {
|
|
1318
|
+
initialOpacity: 0
|
|
1319
|
+
},
|
|
1320
|
+
scale: {
|
|
1321
|
+
from: 0.95
|
|
1322
|
+
},
|
|
1323
|
+
bounce: {
|
|
1324
|
+
intensity: 0.3,
|
|
1325
|
+
easing: "cubic-bezier(0.34, 1.56, 0.64, 1)"
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
stagger: {
|
|
1329
|
+
perItem: 100,
|
|
1330
|
+
baseDelay: 0
|
|
1331
|
+
},
|
|
1332
|
+
interaction: {
|
|
1333
|
+
hover: {
|
|
1334
|
+
scale: 1.05,
|
|
1335
|
+
y: -2,
|
|
1336
|
+
duration: 200,
|
|
1337
|
+
easing: "ease-out"
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
spring: {
|
|
1341
|
+
mass: 1,
|
|
1342
|
+
stiffness: 100,
|
|
1343
|
+
damping: 10,
|
|
1344
|
+
restDelta: 0.01,
|
|
1345
|
+
restSpeed: 0.01
|
|
1346
|
+
},
|
|
1347
|
+
reducedMotion: "fade-only"
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// src/profiles/hua.ts
|
|
1351
|
+
var hua = {
|
|
1352
|
+
name: "hua",
|
|
1353
|
+
base: {
|
|
1354
|
+
duration: 640,
|
|
1355
|
+
easing: "cubic-bezier(0.22, 0.68, 0.35, 1.10)",
|
|
1356
|
+
threshold: 0.12,
|
|
1357
|
+
triggerOnce: true
|
|
1358
|
+
},
|
|
1359
|
+
entrance: {
|
|
1360
|
+
slide: {
|
|
1361
|
+
distance: 28,
|
|
1362
|
+
easing: "cubic-bezier(0.22, 0.68, 0.35, 1.14)"
|
|
1363
|
+
},
|
|
1364
|
+
fade: {
|
|
1365
|
+
initialOpacity: 0
|
|
1366
|
+
},
|
|
1367
|
+
scale: {
|
|
1368
|
+
from: 0.97
|
|
1369
|
+
},
|
|
1370
|
+
bounce: {
|
|
1371
|
+
intensity: 0.2,
|
|
1372
|
+
easing: "cubic-bezier(0.22, 0.68, 0.35, 1.12)"
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
stagger: {
|
|
1376
|
+
perItem: 80,
|
|
1377
|
+
baseDelay: 0
|
|
1378
|
+
},
|
|
1379
|
+
interaction: {
|
|
1380
|
+
hover: {
|
|
1381
|
+
scale: 1.008,
|
|
1382
|
+
y: -1,
|
|
1383
|
+
duration: 180,
|
|
1384
|
+
easing: "cubic-bezier(0.22, 0.68, 0.35, 1.10)"
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
spring: {
|
|
1388
|
+
mass: 1,
|
|
1389
|
+
stiffness: 180,
|
|
1390
|
+
damping: 18,
|
|
1391
|
+
restDelta: 5e-3,
|
|
1392
|
+
restSpeed: 5e-3
|
|
1393
|
+
},
|
|
1394
|
+
reducedMotion: "fade-only"
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
// src/profiles/index.ts
|
|
1398
|
+
var PROFILES = {
|
|
1399
|
+
neutral,
|
|
1400
|
+
hua
|
|
1401
|
+
};
|
|
1402
|
+
function resolveProfile(profile) {
|
|
1403
|
+
if (typeof profile === "string") {
|
|
1404
|
+
return PROFILES[profile] ?? neutral;
|
|
1659
1405
|
}
|
|
1406
|
+
return profile;
|
|
1660
1407
|
}
|
|
1661
|
-
function
|
|
1662
|
-
return
|
|
1408
|
+
function mergeProfileOverrides(base, overrides) {
|
|
1409
|
+
return deepMerge(
|
|
1410
|
+
base,
|
|
1411
|
+
overrides
|
|
1412
|
+
);
|
|
1663
1413
|
}
|
|
1664
|
-
function
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1414
|
+
function deepMerge(target, source) {
|
|
1415
|
+
const result = { ...target };
|
|
1416
|
+
for (const key of Object.keys(source)) {
|
|
1417
|
+
const sourceVal = source[key];
|
|
1418
|
+
const targetVal = result[key];
|
|
1419
|
+
if (sourceVal !== null && sourceVal !== void 0 && typeof sourceVal === "object" && !Array.isArray(sourceVal) && typeof targetVal === "object" && targetVal !== null && !Array.isArray(targetVal)) {
|
|
1420
|
+
result[key] = deepMerge(
|
|
1421
|
+
targetVal,
|
|
1422
|
+
sourceVal
|
|
1423
|
+
);
|
|
1424
|
+
} else if (sourceVal !== void 0) {
|
|
1425
|
+
result[key] = sourceVal;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return result;
|
|
1668
1429
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1430
|
+
|
|
1431
|
+
// src/profiles/MotionProfileContext.ts
|
|
1432
|
+
var MotionProfileContext = createContext(neutral);
|
|
1433
|
+
function MotionProfileProvider({
|
|
1434
|
+
profile = "neutral",
|
|
1435
|
+
overrides,
|
|
1436
|
+
children
|
|
1437
|
+
}) {
|
|
1438
|
+
const resolved = useMemo(() => {
|
|
1439
|
+
const base = resolveProfile(profile);
|
|
1440
|
+
return overrides ? mergeProfileOverrides(base, overrides) : base;
|
|
1441
|
+
}, [profile, overrides]);
|
|
1442
|
+
return createElement(
|
|
1443
|
+
MotionProfileContext.Provider,
|
|
1444
|
+
{ value: resolved },
|
|
1445
|
+
children
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
function useMotionProfile() {
|
|
1449
|
+
return useContext(MotionProfileContext);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/utils/sharedIntersectionObserver.ts
|
|
1453
|
+
var pool = /* @__PURE__ */ new Map();
|
|
1454
|
+
function makeKey(threshold, rootMargin) {
|
|
1455
|
+
const th = Array.isArray(threshold) ? threshold.join(",") : String(threshold);
|
|
1456
|
+
return `${th}|${rootMargin}`;
|
|
1457
|
+
}
|
|
1458
|
+
function handleIntersections(group, entries) {
|
|
1459
|
+
for (const entry of entries) {
|
|
1460
|
+
const subs = group.elements.get(entry.target);
|
|
1461
|
+
if (!subs) continue;
|
|
1462
|
+
const snapshot = [...subs];
|
|
1463
|
+
for (const sub of snapshot) {
|
|
1464
|
+
sub.callback(entry);
|
|
1465
|
+
if (sub.once && entry.isIntersecting) {
|
|
1466
|
+
removeSub(group, entry.target, sub);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function removeSub(group, element, sub) {
|
|
1472
|
+
const subs = group.elements.get(element);
|
|
1473
|
+
if (!subs) return;
|
|
1474
|
+
const idx = subs.indexOf(sub);
|
|
1475
|
+
if (idx >= 0) subs.splice(idx, 1);
|
|
1476
|
+
if (subs.length === 0) {
|
|
1477
|
+
group.elements.delete(element);
|
|
1478
|
+
group.observer.unobserve(element);
|
|
1479
|
+
if (group.elements.size === 0) {
|
|
1480
|
+
group.observer.disconnect();
|
|
1481
|
+
for (const [key, g] of pool) {
|
|
1482
|
+
if (g === group) {
|
|
1483
|
+
pool.delete(key);
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function observeElement(element, callback, options, once) {
|
|
1491
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
1492
|
+
return () => {
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
const threshold = options?.threshold ?? 0;
|
|
1496
|
+
const rootMargin = options?.rootMargin ?? "0px";
|
|
1497
|
+
const key = makeKey(threshold, rootMargin);
|
|
1498
|
+
let group = pool.get(key);
|
|
1499
|
+
if (!group) {
|
|
1500
|
+
const observer = new IntersectionObserver(
|
|
1501
|
+
(entries) => handleIntersections(group, entries),
|
|
1502
|
+
{ threshold, rootMargin }
|
|
1503
|
+
);
|
|
1504
|
+
group = { observer, elements: /* @__PURE__ */ new Map() };
|
|
1505
|
+
pool.set(key, group);
|
|
1506
|
+
}
|
|
1507
|
+
const sub = { callback, once: once ?? false };
|
|
1508
|
+
const subs = group.elements.get(element);
|
|
1509
|
+
if (subs) {
|
|
1510
|
+
subs.push(sub);
|
|
1511
|
+
} else {
|
|
1512
|
+
group.elements.set(element, [sub]);
|
|
1513
|
+
group.observer.observe(element);
|
|
1514
|
+
}
|
|
1515
|
+
let unsubscribed = false;
|
|
1516
|
+
return () => {
|
|
1517
|
+
if (unsubscribed) return;
|
|
1518
|
+
unsubscribed = true;
|
|
1519
|
+
removeSub(group, element, sub);
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/hooks/useUnifiedMotion.ts
|
|
1524
|
+
function getInitialStyle(type, distance) {
|
|
1525
|
+
switch (type) {
|
|
1526
|
+
case "slideUp":
|
|
1527
|
+
return { opacity: 0, transform: `translateY(${distance}px)` };
|
|
1528
|
+
case "slideLeft":
|
|
1529
|
+
return { opacity: 0, transform: `translateX(${distance}px)` };
|
|
1530
|
+
case "slideRight":
|
|
1531
|
+
return { opacity: 0, transform: `translateX(-${distance}px)` };
|
|
1532
|
+
case "scaleIn":
|
|
1533
|
+
return { opacity: 0, transform: "scale(0)" };
|
|
1534
|
+
case "bounceIn":
|
|
1535
|
+
return { opacity: 0, transform: "scale(0)" };
|
|
1536
|
+
case "fadeIn":
|
|
1537
|
+
default:
|
|
1538
|
+
return { opacity: 0, transform: "none" };
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function getVisibleStyle() {
|
|
1542
|
+
return { opacity: 1, transform: "none" };
|
|
1543
|
+
}
|
|
1544
|
+
function getEasingForType(type, easing2) {
|
|
1545
|
+
if (easing2) return easing2;
|
|
1546
|
+
if (type === "bounceIn") return "cubic-bezier(0.34, 1.56, 0.64, 1)";
|
|
1547
|
+
return "ease-out";
|
|
1548
|
+
}
|
|
1549
|
+
function getMultiEffectInitialStyle(effects, defaultDistance) {
|
|
1550
|
+
const style = {};
|
|
1551
|
+
const transforms = [];
|
|
1552
|
+
if (effects.fade) {
|
|
1553
|
+
style.opacity = 0;
|
|
1554
|
+
}
|
|
1555
|
+
if (effects.slide) {
|
|
1556
|
+
const config = typeof effects.slide === "object" ? effects.slide : {};
|
|
1557
|
+
const direction = config.direction ?? "up";
|
|
1558
|
+
const distance = config.distance ?? defaultDistance;
|
|
1559
|
+
switch (direction) {
|
|
1560
|
+
case "up":
|
|
1561
|
+
transforms.push(`translateY(${distance}px)`);
|
|
1562
|
+
break;
|
|
1563
|
+
case "down":
|
|
1564
|
+
transforms.push(`translateY(-${distance}px)`);
|
|
1565
|
+
break;
|
|
1566
|
+
case "left":
|
|
1567
|
+
transforms.push(`translateX(${distance}px)`);
|
|
1568
|
+
break;
|
|
1569
|
+
case "right":
|
|
1570
|
+
transforms.push(`translateX(-${distance}px)`);
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
if (!effects.fade) style.opacity = 0;
|
|
1574
|
+
}
|
|
1575
|
+
if (effects.scale) {
|
|
1576
|
+
const config = typeof effects.scale === "object" ? effects.scale : {};
|
|
1577
|
+
const from = config.from ?? 0.95;
|
|
1578
|
+
transforms.push(`scale(${from})`);
|
|
1579
|
+
if (!effects.fade && !effects.slide) style.opacity = 0;
|
|
1580
|
+
}
|
|
1581
|
+
if (effects.bounce) {
|
|
1582
|
+
transforms.push("scale(0)");
|
|
1583
|
+
if (!effects.fade && !effects.slide && !effects.scale) style.opacity = 0;
|
|
1584
|
+
}
|
|
1585
|
+
if (transforms.length > 0) {
|
|
1586
|
+
style.transform = transforms.join(" ");
|
|
1587
|
+
} else {
|
|
1588
|
+
style.transform = "none";
|
|
1589
|
+
}
|
|
1590
|
+
if (effects.fade && transforms.length === 0) {
|
|
1591
|
+
style.transform = "none";
|
|
1592
|
+
}
|
|
1593
|
+
return style;
|
|
1594
|
+
}
|
|
1595
|
+
function getMultiEffectVisibleStyle(effects) {
|
|
1596
|
+
const style = {};
|
|
1597
|
+
if (effects.fade) {
|
|
1598
|
+
const config = typeof effects.fade === "object" ? effects.fade : {};
|
|
1599
|
+
style.opacity = config.targetOpacity ?? 1;
|
|
1600
|
+
} else {
|
|
1601
|
+
style.opacity = 1;
|
|
1602
|
+
}
|
|
1603
|
+
if (effects.scale) {
|
|
1604
|
+
const config = typeof effects.scale === "object" ? effects.scale : {};
|
|
1605
|
+
style.transform = `scale(${config.to ?? 1})`;
|
|
1606
|
+
} else {
|
|
1607
|
+
style.transform = "none";
|
|
1608
|
+
}
|
|
1609
|
+
return style;
|
|
1610
|
+
}
|
|
1611
|
+
function getMultiEffectEasing(effects, easing2) {
|
|
1612
|
+
if (easing2) return easing2;
|
|
1613
|
+
if (effects.bounce) return "cubic-bezier(0.34, 1.56, 0.64, 1)";
|
|
1614
|
+
return "ease-out";
|
|
1615
|
+
}
|
|
1616
|
+
function useUnifiedMotion(options) {
|
|
1617
|
+
const profile = useMotionProfile();
|
|
1618
|
+
const {
|
|
1619
|
+
type,
|
|
1620
|
+
effects,
|
|
1621
|
+
duration = profile.base.duration,
|
|
1673
1622
|
autoStart = true,
|
|
1674
1623
|
delay = 0,
|
|
1675
1624
|
easing: easing2,
|
|
1676
|
-
threshold =
|
|
1677
|
-
triggerOnce =
|
|
1678
|
-
distance =
|
|
1625
|
+
threshold = profile.base.threshold,
|
|
1626
|
+
triggerOnce = profile.base.triggerOnce,
|
|
1627
|
+
distance = profile.entrance.slide.distance,
|
|
1679
1628
|
onComplete,
|
|
1680
1629
|
onStart,
|
|
1681
1630
|
onStop,
|
|
1682
1631
|
onReset
|
|
1683
1632
|
} = options;
|
|
1684
|
-
const
|
|
1633
|
+
const resolvedType = type ?? "fadeIn";
|
|
1634
|
+
const resolvedEasing = getEasingForType(resolvedType, easing2);
|
|
1685
1635
|
const ref = useRef(null);
|
|
1686
1636
|
const [isVisible, setIsVisible] = useState(false);
|
|
1687
1637
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
1688
1638
|
const [progress, setProgress] = useState(0);
|
|
1689
|
-
const observerRef = useRef(null);
|
|
1690
1639
|
const timeoutRef = useRef(null);
|
|
1691
1640
|
const startRef = useRef(() => {
|
|
1692
1641
|
});
|
|
@@ -1719,29 +1668,32 @@ function useUnifiedMotion(options) {
|
|
|
1719
1668
|
}, [stop, onReset]);
|
|
1720
1669
|
useEffect(() => {
|
|
1721
1670
|
if (!ref.current || !autoStart) return;
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
startRef.current();
|
|
1727
|
-
if (triggerOnce) {
|
|
1728
|
-
observerRef.current?.disconnect();
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
});
|
|
1671
|
+
return observeElement(
|
|
1672
|
+
ref.current,
|
|
1673
|
+
(entry) => {
|
|
1674
|
+
if (entry.isIntersecting) startRef.current();
|
|
1732
1675
|
},
|
|
1733
|
-
{ threshold }
|
|
1676
|
+
{ threshold },
|
|
1677
|
+
triggerOnce
|
|
1734
1678
|
);
|
|
1735
|
-
observerRef.current.observe(ref.current);
|
|
1736
|
-
return () => {
|
|
1737
|
-
observerRef.current?.disconnect();
|
|
1738
|
-
};
|
|
1739
1679
|
}, [autoStart, threshold, triggerOnce]);
|
|
1740
1680
|
useEffect(() => {
|
|
1741
1681
|
return () => stop();
|
|
1742
1682
|
}, [stop]);
|
|
1743
1683
|
const style = useMemo(() => {
|
|
1744
|
-
|
|
1684
|
+
if (effects) {
|
|
1685
|
+
const base2 = isVisible ? getMultiEffectVisibleStyle(effects) : getMultiEffectInitialStyle(effects, distance);
|
|
1686
|
+
const resolvedEasingMulti = getMultiEffectEasing(effects, easing2);
|
|
1687
|
+
return {
|
|
1688
|
+
...base2,
|
|
1689
|
+
transition: `all ${duration}ms ${resolvedEasingMulti}`,
|
|
1690
|
+
"--motion-delay": `${delay}ms`,
|
|
1691
|
+
"--motion-duration": `${duration}ms`,
|
|
1692
|
+
"--motion-easing": resolvedEasingMulti,
|
|
1693
|
+
"--motion-progress": `${progress}`
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
const base = isVisible ? getVisibleStyle() : getInitialStyle(resolvedType, distance);
|
|
1745
1697
|
return {
|
|
1746
1698
|
...base,
|
|
1747
1699
|
transition: `all ${duration}ms ${resolvedEasing}`,
|
|
@@ -1750,7 +1702,7 @@ function useUnifiedMotion(options) {
|
|
|
1750
1702
|
"--motion-easing": resolvedEasing,
|
|
1751
1703
|
"--motion-progress": `${progress}`
|
|
1752
1704
|
};
|
|
1753
|
-
}, [isVisible, type, distance, duration, resolvedEasing, delay, progress]);
|
|
1705
|
+
}, [isVisible, type, effects, distance, duration, resolvedEasing, easing2, delay, progress, resolvedType]);
|
|
1754
1706
|
return {
|
|
1755
1707
|
ref,
|
|
1756
1708
|
isVisible,
|
|
@@ -1763,14 +1715,15 @@ function useUnifiedMotion(options) {
|
|
|
1763
1715
|
};
|
|
1764
1716
|
}
|
|
1765
1717
|
function useFadeIn(options = {}) {
|
|
1718
|
+
const profile = useMotionProfile();
|
|
1766
1719
|
const {
|
|
1767
1720
|
delay = 0,
|
|
1768
|
-
duration =
|
|
1769
|
-
threshold =
|
|
1770
|
-
triggerOnce =
|
|
1771
|
-
easing: easing2 =
|
|
1721
|
+
duration = profile.base.duration,
|
|
1722
|
+
threshold = profile.base.threshold,
|
|
1723
|
+
triggerOnce = profile.base.triggerOnce,
|
|
1724
|
+
easing: easing2 = profile.base.easing,
|
|
1772
1725
|
autoStart = true,
|
|
1773
|
-
initialOpacity =
|
|
1726
|
+
initialOpacity = profile.entrance.fade.initialOpacity,
|
|
1774
1727
|
targetOpacity = 1,
|
|
1775
1728
|
onComplete,
|
|
1776
1729
|
onStart,
|
|
@@ -1782,7 +1735,6 @@ function useFadeIn(options = {}) {
|
|
|
1782
1735
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
1783
1736
|
const [progress, setProgress] = useState(0);
|
|
1784
1737
|
const [nodeReady, setNodeReady] = useState(false);
|
|
1785
|
-
const observerRef = useRef(null);
|
|
1786
1738
|
const motionRef = useRef(null);
|
|
1787
1739
|
const timeoutRef = useRef(null);
|
|
1788
1740
|
const startRef = useRef(() => {
|
|
@@ -1841,31 +1793,15 @@ function useFadeIn(options = {}) {
|
|
|
1841
1793
|
}, [stop, onReset]);
|
|
1842
1794
|
useEffect(() => {
|
|
1843
1795
|
if (!ref.current || !autoStart) return;
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
startRef.current();
|
|
1849
|
-
if (triggerOnce) {
|
|
1850
|
-
observerRef.current?.disconnect();
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
});
|
|
1796
|
+
return observeElement(
|
|
1797
|
+
ref.current,
|
|
1798
|
+
(entry) => {
|
|
1799
|
+
if (entry.isIntersecting) startRef.current();
|
|
1854
1800
|
},
|
|
1855
|
-
{ threshold }
|
|
1801
|
+
{ threshold },
|
|
1802
|
+
triggerOnce
|
|
1856
1803
|
);
|
|
1857
|
-
observerRef.current.observe(ref.current);
|
|
1858
|
-
return () => {
|
|
1859
|
-
if (observerRef.current) {
|
|
1860
|
-
observerRef.current.disconnect();
|
|
1861
|
-
}
|
|
1862
|
-
};
|
|
1863
1804
|
}, [autoStart, threshold, triggerOnce, nodeReady]);
|
|
1864
|
-
useEffect(() => {
|
|
1865
|
-
if (!autoStart) {
|
|
1866
|
-
start();
|
|
1867
|
-
}
|
|
1868
|
-
}, [autoStart, start]);
|
|
1869
1805
|
useEffect(() => {
|
|
1870
1806
|
return () => {
|
|
1871
1807
|
stop();
|
|
@@ -1891,15 +1827,16 @@ function useFadeIn(options = {}) {
|
|
|
1891
1827
|
};
|
|
1892
1828
|
}
|
|
1893
1829
|
function useSlideUp(options = {}) {
|
|
1830
|
+
const profile = useMotionProfile();
|
|
1894
1831
|
const {
|
|
1895
1832
|
delay = 0,
|
|
1896
|
-
duration =
|
|
1897
|
-
threshold =
|
|
1898
|
-
triggerOnce =
|
|
1899
|
-
easing: easing2 =
|
|
1833
|
+
duration = profile.base.duration,
|
|
1834
|
+
threshold = profile.base.threshold,
|
|
1835
|
+
triggerOnce = profile.base.triggerOnce,
|
|
1836
|
+
easing: easing2 = profile.entrance.slide.easing,
|
|
1900
1837
|
autoStart = true,
|
|
1901
1838
|
direction = "up",
|
|
1902
|
-
distance =
|
|
1839
|
+
distance = profile.entrance.slide.distance,
|
|
1903
1840
|
onComplete,
|
|
1904
1841
|
onStart,
|
|
1905
1842
|
onStop,
|
|
@@ -1910,7 +1847,6 @@ function useSlideUp(options = {}) {
|
|
|
1910
1847
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
1911
1848
|
const [progress, setProgress] = useState(0);
|
|
1912
1849
|
const [nodeReady, setNodeReady] = useState(false);
|
|
1913
|
-
const observerRef = useRef(null);
|
|
1914
1850
|
const timeoutRef = useRef(null);
|
|
1915
1851
|
const startRef = useRef(() => {
|
|
1916
1852
|
});
|
|
@@ -1928,7 +1864,7 @@ function useSlideUp(options = {}) {
|
|
|
1928
1864
|
}, 50);
|
|
1929
1865
|
return () => clearInterval(id);
|
|
1930
1866
|
}, [nodeReady]);
|
|
1931
|
-
const
|
|
1867
|
+
const getInitialTransform2 = useCallback(() => {
|
|
1932
1868
|
switch (direction) {
|
|
1933
1869
|
case "up":
|
|
1934
1870
|
return `translateY(${distance}px)`;
|
|
@@ -1978,37 +1914,21 @@ function useSlideUp(options = {}) {
|
|
|
1978
1914
|
}, [stop, onReset]);
|
|
1979
1915
|
useEffect(() => {
|
|
1980
1916
|
if (!ref.current || !autoStart) return;
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
startRef.current();
|
|
1986
|
-
if (triggerOnce) {
|
|
1987
|
-
observerRef.current?.disconnect();
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
});
|
|
1917
|
+
return observeElement(
|
|
1918
|
+
ref.current,
|
|
1919
|
+
(entry) => {
|
|
1920
|
+
if (entry.isIntersecting) startRef.current();
|
|
1991
1921
|
},
|
|
1992
|
-
{ threshold }
|
|
1922
|
+
{ threshold },
|
|
1923
|
+
triggerOnce
|
|
1993
1924
|
);
|
|
1994
|
-
observerRef.current.observe(ref.current);
|
|
1995
|
-
return () => {
|
|
1996
|
-
if (observerRef.current) {
|
|
1997
|
-
observerRef.current.disconnect();
|
|
1998
|
-
}
|
|
1999
|
-
};
|
|
2000
1925
|
}, [autoStart, threshold, triggerOnce, nodeReady]);
|
|
2001
|
-
useEffect(() => {
|
|
2002
|
-
if (!autoStart) {
|
|
2003
|
-
start();
|
|
2004
|
-
}
|
|
2005
|
-
}, [autoStart, start]);
|
|
2006
1926
|
useEffect(() => {
|
|
2007
1927
|
return () => {
|
|
2008
1928
|
stop();
|
|
2009
1929
|
};
|
|
2010
1930
|
}, [stop]);
|
|
2011
|
-
const initialTransform = useMemo(() =>
|
|
1931
|
+
const initialTransform = useMemo(() => getInitialTransform2(), [getInitialTransform2]);
|
|
2012
1932
|
const finalTransform = useMemo(() => {
|
|
2013
1933
|
return direction === "left" || direction === "right" ? "translateX(0)" : "translateY(0)";
|
|
2014
1934
|
}, [direction]);
|
|
@@ -2045,15 +1965,16 @@ function useSlideRight(options = {}) {
|
|
|
2045
1965
|
return useSlideUp({ ...options, direction: "right" });
|
|
2046
1966
|
}
|
|
2047
1967
|
function useScaleIn(options = {}) {
|
|
1968
|
+
const profile = useMotionProfile();
|
|
2048
1969
|
const {
|
|
2049
1970
|
initialScale = 0,
|
|
2050
1971
|
targetScale = 1,
|
|
2051
|
-
duration =
|
|
1972
|
+
duration = profile.base.duration,
|
|
2052
1973
|
delay = 0,
|
|
2053
1974
|
autoStart = true,
|
|
2054
|
-
easing: easing2 =
|
|
2055
|
-
threshold =
|
|
2056
|
-
triggerOnce =
|
|
1975
|
+
easing: easing2 = profile.base.easing,
|
|
1976
|
+
threshold = profile.base.threshold,
|
|
1977
|
+
triggerOnce = profile.base.triggerOnce,
|
|
2057
1978
|
onComplete,
|
|
2058
1979
|
onStart,
|
|
2059
1980
|
onStop,
|
|
@@ -2065,7 +1986,6 @@ function useScaleIn(options = {}) {
|
|
|
2065
1986
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
2066
1987
|
const [isVisible, setIsVisible] = useState(autoStart ? false : true);
|
|
2067
1988
|
const [progress, setProgress] = useState(autoStart ? 0 : 1);
|
|
2068
|
-
const observerRef = useRef(null);
|
|
2069
1989
|
const timeoutRef = useRef(null);
|
|
2070
1990
|
const startRef = useRef(() => {
|
|
2071
1991
|
});
|
|
@@ -2113,25 +2033,14 @@ function useScaleIn(options = {}) {
|
|
|
2113
2033
|
}, [stop, initialScale, onReset]);
|
|
2114
2034
|
useEffect(() => {
|
|
2115
2035
|
if (!ref.current || !autoStart) return;
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
startRef.current();
|
|
2121
|
-
if (triggerOnce) {
|
|
2122
|
-
observerRef.current?.disconnect();
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
});
|
|
2036
|
+
return observeElement(
|
|
2037
|
+
ref.current,
|
|
2038
|
+
(entry) => {
|
|
2039
|
+
if (entry.isIntersecting) startRef.current();
|
|
2126
2040
|
},
|
|
2127
|
-
{ threshold }
|
|
2041
|
+
{ threshold },
|
|
2042
|
+
triggerOnce
|
|
2128
2043
|
);
|
|
2129
|
-
observerRef.current.observe(ref.current);
|
|
2130
|
-
return () => {
|
|
2131
|
-
if (observerRef.current) {
|
|
2132
|
-
observerRef.current.disconnect();
|
|
2133
|
-
}
|
|
2134
|
-
};
|
|
2135
2044
|
}, [autoStart, threshold, triggerOnce]);
|
|
2136
2045
|
useEffect(() => {
|
|
2137
2046
|
return () => {
|
|
@@ -2159,15 +2068,15 @@ function useScaleIn(options = {}) {
|
|
|
2159
2068
|
};
|
|
2160
2069
|
}
|
|
2161
2070
|
function useBounceIn(options = {}) {
|
|
2071
|
+
const profile = useMotionProfile();
|
|
2162
2072
|
const {
|
|
2163
2073
|
duration = 600,
|
|
2164
2074
|
delay = 0,
|
|
2165
2075
|
autoStart = true,
|
|
2166
|
-
intensity =
|
|
2167
|
-
threshold =
|
|
2168
|
-
triggerOnce =
|
|
2169
|
-
easing: easing2 =
|
|
2170
|
-
// 바운스 이징
|
|
2076
|
+
intensity = profile.entrance.bounce.intensity,
|
|
2077
|
+
threshold = profile.base.threshold,
|
|
2078
|
+
triggerOnce = profile.base.triggerOnce,
|
|
2079
|
+
easing: easing2 = profile.entrance.bounce.easing,
|
|
2171
2080
|
onComplete,
|
|
2172
2081
|
onStart,
|
|
2173
2082
|
onStop,
|
|
@@ -2179,7 +2088,6 @@ function useBounceIn(options = {}) {
|
|
|
2179
2088
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
2180
2089
|
const [isVisible, setIsVisible] = useState(autoStart ? false : true);
|
|
2181
2090
|
const [progress, setProgress] = useState(autoStart ? 0 : 1);
|
|
2182
|
-
const observerRef = useRef(null);
|
|
2183
2091
|
const timeoutRef = useRef(null);
|
|
2184
2092
|
const bounceTimeoutRef = useRef(null);
|
|
2185
2093
|
const startRef = useRef(() => {
|
|
@@ -2236,25 +2144,14 @@ function useBounceIn(options = {}) {
|
|
|
2236
2144
|
}, [stop, onReset]);
|
|
2237
2145
|
useEffect(() => {
|
|
2238
2146
|
if (!ref.current || !autoStart) return;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
startRef.current();
|
|
2244
|
-
if (triggerOnce) {
|
|
2245
|
-
observerRef.current?.disconnect();
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
});
|
|
2147
|
+
return observeElement(
|
|
2148
|
+
ref.current,
|
|
2149
|
+
(entry) => {
|
|
2150
|
+
if (entry.isIntersecting) startRef.current();
|
|
2249
2151
|
},
|
|
2250
|
-
{ threshold }
|
|
2152
|
+
{ threshold },
|
|
2153
|
+
triggerOnce
|
|
2251
2154
|
);
|
|
2252
|
-
observerRef.current.observe(ref.current);
|
|
2253
|
-
return () => {
|
|
2254
|
-
if (observerRef.current) {
|
|
2255
|
-
observerRef.current.disconnect();
|
|
2256
|
-
}
|
|
2257
|
-
};
|
|
2258
2155
|
}, [autoStart, threshold, triggerOnce]);
|
|
2259
2156
|
useEffect(() => {
|
|
2260
2157
|
return () => {
|
|
@@ -2579,18 +2476,33 @@ function usePulse(options = {}) {
|
|
|
2579
2476
|
reset
|
|
2580
2477
|
};
|
|
2581
2478
|
}
|
|
2479
|
+
|
|
2480
|
+
// src/utils/springPhysics.ts
|
|
2481
|
+
function calculateSpring(currentValue, currentVelocity, targetValue, deltaTime, config) {
|
|
2482
|
+
const { stiffness, damping, mass } = config;
|
|
2483
|
+
const displacement = currentValue - targetValue;
|
|
2484
|
+
const springForce = -stiffness * displacement;
|
|
2485
|
+
const dampingForce = -damping * currentVelocity;
|
|
2486
|
+
const acceleration = (springForce + dampingForce) / mass;
|
|
2487
|
+
const newVelocity = currentVelocity + acceleration * deltaTime;
|
|
2488
|
+
const newValue = currentValue + newVelocity * deltaTime;
|
|
2489
|
+
return { value: newValue, velocity: newVelocity };
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/hooks/useSpringMotion.ts
|
|
2582
2493
|
function useSpringMotion(options) {
|
|
2494
|
+
const profile = useMotionProfile();
|
|
2583
2495
|
const {
|
|
2584
2496
|
from,
|
|
2585
2497
|
to,
|
|
2586
|
-
mass =
|
|
2587
|
-
stiffness =
|
|
2588
|
-
damping =
|
|
2589
|
-
restDelta =
|
|
2590
|
-
restSpeed =
|
|
2498
|
+
mass = profile.spring.mass,
|
|
2499
|
+
stiffness = profile.spring.stiffness,
|
|
2500
|
+
damping = profile.spring.damping,
|
|
2501
|
+
restDelta = profile.spring.restDelta,
|
|
2502
|
+
restSpeed = profile.spring.restSpeed,
|
|
2591
2503
|
onComplete,
|
|
2592
2504
|
enabled = true,
|
|
2593
|
-
autoStart =
|
|
2505
|
+
autoStart = true
|
|
2594
2506
|
} = options;
|
|
2595
2507
|
const ref = useRef(null);
|
|
2596
2508
|
const [springState, setSpringState] = useState({
|
|
@@ -2602,16 +2514,7 @@ function useSpringMotion(options) {
|
|
|
2602
2514
|
const [progress, setProgress] = useState(0);
|
|
2603
2515
|
const motionRef = useRef(null);
|
|
2604
2516
|
const lastTimeRef = useRef(0);
|
|
2605
|
-
const
|
|
2606
|
-
const displacement = currentValue - targetValue;
|
|
2607
|
-
const springForce = -stiffness * displacement;
|
|
2608
|
-
const dampingForce = -damping * currentVelocity;
|
|
2609
|
-
const totalForce = springForce + dampingForce;
|
|
2610
|
-
const acceleration = totalForce / mass;
|
|
2611
|
-
const newVelocity = currentVelocity + acceleration * deltaTime;
|
|
2612
|
-
const newValue = currentValue + newVelocity * deltaTime;
|
|
2613
|
-
return { value: newValue, velocity: newVelocity };
|
|
2614
|
-
}, [mass, stiffness, damping]);
|
|
2517
|
+
const springConfig = useMemo(() => ({ stiffness, damping, mass }), [stiffness, damping, mass]);
|
|
2615
2518
|
const animate = useCallback((currentTime) => {
|
|
2616
2519
|
if (!enabled || !springState.isAnimating) return;
|
|
2617
2520
|
const deltaTime = Math.min(currentTime - lastTimeRef.current, 16) / 1e3;
|
|
@@ -2620,7 +2523,8 @@ function useSpringMotion(options) {
|
|
|
2620
2523
|
springState.value,
|
|
2621
2524
|
springState.velocity,
|
|
2622
2525
|
to,
|
|
2623
|
-
deltaTime
|
|
2526
|
+
deltaTime,
|
|
2527
|
+
springConfig
|
|
2624
2528
|
);
|
|
2625
2529
|
const range = Math.abs(to - from);
|
|
2626
2530
|
const currentProgress = range > 0 ? Math.min(Math.abs(value - from) / range, 1) : 1;
|
|
@@ -2642,7 +2546,7 @@ function useSpringMotion(options) {
|
|
|
2642
2546
|
isAnimating: true
|
|
2643
2547
|
});
|
|
2644
2548
|
motionRef.current = requestAnimationFrame(animate);
|
|
2645
|
-
}, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete,
|
|
2549
|
+
}, [enabled, springState.isAnimating, to, from, restDelta, restSpeed, onComplete, springConfig]);
|
|
2646
2550
|
const start = useCallback(() => {
|
|
2647
2551
|
if (springState.isAnimating) return;
|
|
2648
2552
|
setSpringState((prev) => ({
|
|
@@ -2790,11 +2694,12 @@ function useGradient(options = {}) {
|
|
|
2790
2694
|
};
|
|
2791
2695
|
}
|
|
2792
2696
|
function useHoverMotion(options = {}) {
|
|
2697
|
+
const profile = useMotionProfile();
|
|
2793
2698
|
const {
|
|
2794
|
-
duration =
|
|
2795
|
-
easing: easing2 =
|
|
2796
|
-
hoverScale =
|
|
2797
|
-
hoverY =
|
|
2699
|
+
duration = profile.interaction.hover.duration,
|
|
2700
|
+
easing: easing2 = profile.interaction.hover.easing,
|
|
2701
|
+
hoverScale = profile.interaction.hover.scale,
|
|
2702
|
+
hoverY = profile.interaction.hover.y,
|
|
2798
2703
|
hoverOpacity = 1
|
|
2799
2704
|
} = options;
|
|
2800
2705
|
const ref = useRef(null);
|
|
@@ -3078,13 +2983,14 @@ function useFocusToggle(options = {}) {
|
|
|
3078
2983
|
};
|
|
3079
2984
|
}
|
|
3080
2985
|
function useScrollReveal(options = {}) {
|
|
2986
|
+
const profile = useMotionProfile();
|
|
3081
2987
|
const {
|
|
3082
|
-
threshold =
|
|
2988
|
+
threshold = profile.base.threshold,
|
|
3083
2989
|
rootMargin = "0px",
|
|
3084
|
-
triggerOnce =
|
|
2990
|
+
triggerOnce = profile.base.triggerOnce,
|
|
3085
2991
|
delay = 0,
|
|
3086
|
-
duration =
|
|
3087
|
-
easing: easing2 =
|
|
2992
|
+
duration = profile.base.duration,
|
|
2993
|
+
easing: easing2 = profile.base.easing,
|
|
3088
2994
|
motionType = "fadeIn",
|
|
3089
2995
|
onComplete,
|
|
3090
2996
|
onStart,
|
|
@@ -3113,15 +3019,14 @@ function useScrollReveal(options = {}) {
|
|
|
3113
3019
|
}, [triggerOnce, hasTriggered, delay, onStart, onComplete]);
|
|
3114
3020
|
useEffect(() => {
|
|
3115
3021
|
if (!ref.current) return;
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
return () => {
|
|
3122
|
-
observer.disconnect();
|
|
3123
|
-
};
|
|
3022
|
+
return observeElement(
|
|
3023
|
+
ref.current,
|
|
3024
|
+
(entry) => observerCallback([entry]),
|
|
3025
|
+
{ threshold, rootMargin }
|
|
3026
|
+
);
|
|
3124
3027
|
}, [observerCallback, threshold, rootMargin]);
|
|
3028
|
+
const slideDistance = profile.entrance.slide.distance;
|
|
3029
|
+
const scaleFrom = profile.entrance.scale.from;
|
|
3125
3030
|
const style = useMemo(() => {
|
|
3126
3031
|
const baseTransition = `all ${duration}ms ${easing2}`;
|
|
3127
3032
|
if (!isVisible) {
|
|
@@ -3134,25 +3039,25 @@ function useScrollReveal(options = {}) {
|
|
|
3134
3039
|
case "slideUp":
|
|
3135
3040
|
return {
|
|
3136
3041
|
opacity: 0,
|
|
3137
|
-
transform:
|
|
3042
|
+
transform: `translateY(${slideDistance}px)`,
|
|
3138
3043
|
transition: baseTransition
|
|
3139
3044
|
};
|
|
3140
3045
|
case "slideLeft":
|
|
3141
3046
|
return {
|
|
3142
3047
|
opacity: 0,
|
|
3143
|
-
transform:
|
|
3048
|
+
transform: `translateX(-${slideDistance}px)`,
|
|
3144
3049
|
transition: baseTransition
|
|
3145
3050
|
};
|
|
3146
3051
|
case "slideRight":
|
|
3147
3052
|
return {
|
|
3148
3053
|
opacity: 0,
|
|
3149
|
-
transform:
|
|
3054
|
+
transform: `translateX(${slideDistance}px)`,
|
|
3150
3055
|
transition: baseTransition
|
|
3151
3056
|
};
|
|
3152
3057
|
case "scaleIn":
|
|
3153
3058
|
return {
|
|
3154
3059
|
opacity: 0,
|
|
3155
|
-
transform:
|
|
3060
|
+
transform: `scale(${scaleFrom})`,
|
|
3156
3061
|
transition: baseTransition
|
|
3157
3062
|
};
|
|
3158
3063
|
case "bounceIn":
|
|
@@ -3173,7 +3078,7 @@ function useScrollReveal(options = {}) {
|
|
|
3173
3078
|
transform: "none",
|
|
3174
3079
|
transition: baseTransition
|
|
3175
3080
|
};
|
|
3176
|
-
}, [isVisible, motionType, duration, easing2]);
|
|
3081
|
+
}, [isVisible, motionType, duration, easing2, slideDistance, scaleFrom]);
|
|
3177
3082
|
const start = useCallback(() => {
|
|
3178
3083
|
setIsAnimating(true);
|
|
3179
3084
|
onStart?.();
|
|
@@ -3206,6 +3111,38 @@ function useScrollReveal(options = {}) {
|
|
|
3206
3111
|
stop
|
|
3207
3112
|
};
|
|
3208
3113
|
}
|
|
3114
|
+
|
|
3115
|
+
// src/utils/sharedScroll.ts
|
|
3116
|
+
var subscribers = /* @__PURE__ */ new Set();
|
|
3117
|
+
var listening = false;
|
|
3118
|
+
var rafId = 0;
|
|
3119
|
+
function tick() {
|
|
3120
|
+
subscribers.forEach((cb) => cb());
|
|
3121
|
+
}
|
|
3122
|
+
function onScroll() {
|
|
3123
|
+
cancelAnimationFrame(rafId);
|
|
3124
|
+
rafId = requestAnimationFrame(tick);
|
|
3125
|
+
}
|
|
3126
|
+
function subscribeScroll(cb) {
|
|
3127
|
+
subscribers.add(cb);
|
|
3128
|
+
if (!listening && subscribers.size > 0) {
|
|
3129
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
3130
|
+
window.addEventListener("resize", onScroll, { passive: true });
|
|
3131
|
+
listening = true;
|
|
3132
|
+
}
|
|
3133
|
+
return () => {
|
|
3134
|
+
subscribers.delete(cb);
|
|
3135
|
+
if (listening && subscribers.size === 0) {
|
|
3136
|
+
window.removeEventListener("scroll", onScroll);
|
|
3137
|
+
window.removeEventListener("resize", onScroll);
|
|
3138
|
+
cancelAnimationFrame(rafId);
|
|
3139
|
+
listening = false;
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// src/hooks/useScrollProgress.ts
|
|
3145
|
+
var PROGRESS_THRESHOLD = 0.1;
|
|
3209
3146
|
function useScrollProgress(options = {}) {
|
|
3210
3147
|
const {
|
|
3211
3148
|
target,
|
|
@@ -3214,27 +3151,24 @@ function useScrollProgress(options = {}) {
|
|
|
3214
3151
|
} = options;
|
|
3215
3152
|
const [progress, setProgress] = useState(showOnMount ? 0 : 0);
|
|
3216
3153
|
const [mounted, setMounted] = useState(false);
|
|
3154
|
+
const progressRef = useRef(progress);
|
|
3217
3155
|
useEffect(() => {
|
|
3218
3156
|
setMounted(true);
|
|
3219
3157
|
}, []);
|
|
3220
3158
|
useEffect(() => {
|
|
3221
3159
|
if (!mounted) return;
|
|
3222
3160
|
const calculateProgress = () => {
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3161
|
+
const scrollTop = window.pageYOffset;
|
|
3162
|
+
const scrollHeight = target || document.documentElement.scrollHeight - window.innerHeight;
|
|
3163
|
+
const adjustedScrollTop = Math.max(0, scrollTop - offset);
|
|
3164
|
+
const next = Math.min(100, Math.max(0, adjustedScrollTop / scrollHeight * 100));
|
|
3165
|
+
if (Math.abs(next - progressRef.current) > PROGRESS_THRESHOLD) {
|
|
3166
|
+
progressRef.current = next;
|
|
3167
|
+
setProgress(next);
|
|
3229
3168
|
}
|
|
3230
3169
|
};
|
|
3231
3170
|
calculateProgress();
|
|
3232
|
-
|
|
3233
|
-
window.addEventListener("resize", calculateProgress, { passive: true });
|
|
3234
|
-
return () => {
|
|
3235
|
-
window.removeEventListener("scroll", calculateProgress);
|
|
3236
|
-
window.removeEventListener("resize", calculateProgress);
|
|
3237
|
-
};
|
|
3171
|
+
return subscribeScroll(calculateProgress);
|
|
3238
3172
|
}, [target, offset, mounted]);
|
|
3239
3173
|
return {
|
|
3240
3174
|
progress,
|
|
@@ -3619,14 +3553,11 @@ function useInView(options = {}) {
|
|
|
3619
3553
|
useEffect(() => {
|
|
3620
3554
|
const element = ref.current;
|
|
3621
3555
|
if (!element) return;
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
return () => {
|
|
3628
|
-
observer.disconnect();
|
|
3629
|
-
};
|
|
3556
|
+
return observeElement(
|
|
3557
|
+
element,
|
|
3558
|
+
(entry2) => handleIntersect([entry2]),
|
|
3559
|
+
{ threshold, rootMargin }
|
|
3560
|
+
);
|
|
3630
3561
|
}, [threshold, rootMargin, handleIntersect]);
|
|
3631
3562
|
return {
|
|
3632
3563
|
ref,
|
|
@@ -4188,7 +4119,1671 @@ function useGestureMotion(options) {
|
|
|
4188
4119
|
isActive: gestureState.isActive
|
|
4189
4120
|
};
|
|
4190
4121
|
}
|
|
4122
|
+
function useButtonEffect(options = {}) {
|
|
4123
|
+
const {
|
|
4124
|
+
duration = 200,
|
|
4125
|
+
easing: easing2 = "ease-out",
|
|
4126
|
+
type = "scale",
|
|
4127
|
+
scaleAmount = 0.95,
|
|
4128
|
+
rippleColor = "rgba(255, 255, 255, 0.6)",
|
|
4129
|
+
rippleSize = 100,
|
|
4130
|
+
rippleDuration = 600,
|
|
4131
|
+
glowColor = "#3b82f6",
|
|
4132
|
+
glowSize = 20,
|
|
4133
|
+
glowIntensity = 0.8,
|
|
4134
|
+
shakeAmount = 5,
|
|
4135
|
+
shakeFrequency = 10,
|
|
4136
|
+
bounceHeight = 10,
|
|
4137
|
+
bounceStiffness = 0.3,
|
|
4138
|
+
slideDistance = 5,
|
|
4139
|
+
slideDirection = "down",
|
|
4140
|
+
hoverScale = 1.05,
|
|
4141
|
+
hoverRotate = 0,
|
|
4142
|
+
hoverTranslateY = -2,
|
|
4143
|
+
hoverTranslateX = 0,
|
|
4144
|
+
activeScale = 0.95,
|
|
4145
|
+
activeRotate = 0,
|
|
4146
|
+
activeTranslateY = 2,
|
|
4147
|
+
activeTranslateX = 0,
|
|
4148
|
+
focusScale = 1.02,
|
|
4149
|
+
focusGlow = true,
|
|
4150
|
+
disabled = false,
|
|
4151
|
+
disabledOpacity = 0.5,
|
|
4152
|
+
autoStart = false,
|
|
4153
|
+
onComplete,
|
|
4154
|
+
onStart,
|
|
4155
|
+
onStop,
|
|
4156
|
+
onReset
|
|
4157
|
+
} = options;
|
|
4158
|
+
const ref = useRef(null);
|
|
4159
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
4160
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
4161
|
+
const [progress, setProgress] = useState(0);
|
|
4162
|
+
const [buttonType] = useState(type);
|
|
4163
|
+
const [isPressed, setIsPressed] = useState(false);
|
|
4164
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
4165
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
4166
|
+
const [ripplePosition, setRipplePosition] = useState({ x: 0, y: 0 });
|
|
4167
|
+
const [currentGlowIntensity, setGlowIntensity] = useState(0);
|
|
4168
|
+
const [shakeOffset, setShakeOffset] = useState(0);
|
|
4169
|
+
const [bounceOffset, setBounceOffset] = useState(0);
|
|
4170
|
+
const [slideOffset, setSlideOffset] = useState(0);
|
|
4171
|
+
const animationRef = useRef(null);
|
|
4172
|
+
const startTimeRef = useRef(0);
|
|
4173
|
+
useCallback((event) => {
|
|
4174
|
+
if (!ref.current) return;
|
|
4175
|
+
const rect = ref.current.getBoundingClientRect();
|
|
4176
|
+
const x = event.clientX - rect.left;
|
|
4177
|
+
const y = event.clientY - rect.top;
|
|
4178
|
+
setRipplePosition({ x, y });
|
|
4179
|
+
setIsAnimating(true);
|
|
4180
|
+
setProgress(0);
|
|
4181
|
+
startTimeRef.current = Date.now();
|
|
4182
|
+
onStart?.();
|
|
4183
|
+
const animateRipple = () => {
|
|
4184
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
4185
|
+
const currentProgress = Math.min(elapsed / rippleDuration, 1);
|
|
4186
|
+
setProgress(currentProgress);
|
|
4187
|
+
if (currentProgress < 1) {
|
|
4188
|
+
animationRef.current = requestAnimationFrame(animateRipple);
|
|
4189
|
+
} else {
|
|
4190
|
+
setIsAnimating(false);
|
|
4191
|
+
onComplete?.();
|
|
4192
|
+
}
|
|
4193
|
+
};
|
|
4194
|
+
animationRef.current = requestAnimationFrame(animateRipple);
|
|
4195
|
+
}, [rippleDuration, onStart, onComplete]);
|
|
4196
|
+
const shakeButton = useCallback(() => {
|
|
4197
|
+
setIsAnimating(true);
|
|
4198
|
+
setProgress(0);
|
|
4199
|
+
startTimeRef.current = Date.now();
|
|
4200
|
+
onStart?.();
|
|
4201
|
+
const animateShake = () => {
|
|
4202
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
4203
|
+
const currentProgress = Math.min(elapsed / duration, 1);
|
|
4204
|
+
setProgress(currentProgress);
|
|
4205
|
+
const shake = shakeAmount * Math.sin(currentProgress * Math.PI * 2 * shakeFrequency) * (1 - currentProgress);
|
|
4206
|
+
setShakeOffset(shake);
|
|
4207
|
+
if (currentProgress < 1) {
|
|
4208
|
+
animationRef.current = requestAnimationFrame(animateShake);
|
|
4209
|
+
} else {
|
|
4210
|
+
setIsAnimating(false);
|
|
4211
|
+
setShakeOffset(0);
|
|
4212
|
+
onComplete?.();
|
|
4213
|
+
}
|
|
4214
|
+
};
|
|
4215
|
+
animationRef.current = requestAnimationFrame(animateShake);
|
|
4216
|
+
}, [duration, shakeAmount, shakeFrequency, onStart, onComplete]);
|
|
4217
|
+
const bounceButton = useCallback(() => {
|
|
4218
|
+
setIsAnimating(true);
|
|
4219
|
+
setProgress(0);
|
|
4220
|
+
startTimeRef.current = Date.now();
|
|
4221
|
+
onStart?.();
|
|
4222
|
+
const animateBounce = () => {
|
|
4223
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
4224
|
+
const currentProgress = Math.min(elapsed / duration, 1);
|
|
4225
|
+
setProgress(currentProgress);
|
|
4226
|
+
const bounce = bounceHeight * Math.sin(currentProgress * Math.PI * bounceStiffness) * Math.exp(-currentProgress * 3);
|
|
4227
|
+
setBounceOffset(bounce);
|
|
4228
|
+
if (currentProgress < 1) {
|
|
4229
|
+
animationRef.current = requestAnimationFrame(animateBounce);
|
|
4230
|
+
} else {
|
|
4231
|
+
setIsAnimating(false);
|
|
4232
|
+
setBounceOffset(0);
|
|
4233
|
+
onComplete?.();
|
|
4234
|
+
}
|
|
4235
|
+
};
|
|
4236
|
+
animationRef.current = requestAnimationFrame(animateBounce);
|
|
4237
|
+
}, [duration, bounceHeight, bounceStiffness, onStart, onComplete]);
|
|
4238
|
+
const slideButton = useCallback(() => {
|
|
4239
|
+
setIsAnimating(true);
|
|
4240
|
+
setProgress(0);
|
|
4241
|
+
startTimeRef.current = Date.now();
|
|
4242
|
+
onStart?.();
|
|
4243
|
+
const animateSlide = () => {
|
|
4244
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
4245
|
+
const currentProgress = Math.min(elapsed / duration, 1);
|
|
4246
|
+
setProgress(currentProgress);
|
|
4247
|
+
const slide = slideDistance * currentProgress;
|
|
4248
|
+
setSlideOffset(slide);
|
|
4249
|
+
if (currentProgress < 1) {
|
|
4250
|
+
animationRef.current = requestAnimationFrame(animateSlide);
|
|
4251
|
+
} else {
|
|
4252
|
+
setIsAnimating(false);
|
|
4253
|
+
onComplete?.();
|
|
4254
|
+
}
|
|
4255
|
+
};
|
|
4256
|
+
animationRef.current = requestAnimationFrame(animateSlide);
|
|
4257
|
+
}, [duration, slideDistance, onStart, onComplete]);
|
|
4258
|
+
const pressButton = useCallback(() => {
|
|
4259
|
+
if (disabled) return;
|
|
4260
|
+
setIsPressed(true);
|
|
4261
|
+
switch (type) {
|
|
4262
|
+
case "ripple":
|
|
4263
|
+
break;
|
|
4264
|
+
case "shake":
|
|
4265
|
+
shakeButton();
|
|
4266
|
+
break;
|
|
4267
|
+
case "bounce":
|
|
4268
|
+
bounceButton();
|
|
4269
|
+
break;
|
|
4270
|
+
case "slide":
|
|
4271
|
+
slideButton();
|
|
4272
|
+
break;
|
|
4273
|
+
}
|
|
4274
|
+
}, [disabled, type, shakeButton, bounceButton, slideButton]);
|
|
4275
|
+
const releaseButton = useCallback(() => {
|
|
4276
|
+
setIsPressed(false);
|
|
4277
|
+
}, []);
|
|
4278
|
+
const setButtonState = useCallback((state) => {
|
|
4279
|
+
switch (state) {
|
|
4280
|
+
case "hover":
|
|
4281
|
+
setIsHovered(true);
|
|
4282
|
+
break;
|
|
4283
|
+
case "active":
|
|
4284
|
+
setIsPressed(true);
|
|
4285
|
+
break;
|
|
4286
|
+
case "focus":
|
|
4287
|
+
setIsFocused(true);
|
|
4288
|
+
break;
|
|
4289
|
+
case "disabled":
|
|
4290
|
+
setIsHovered(false);
|
|
4291
|
+
setIsPressed(false);
|
|
4292
|
+
setIsFocused(false);
|
|
4293
|
+
break;
|
|
4294
|
+
default:
|
|
4295
|
+
setIsHovered(false);
|
|
4296
|
+
setIsPressed(false);
|
|
4297
|
+
setIsFocused(false);
|
|
4298
|
+
break;
|
|
4299
|
+
}
|
|
4300
|
+
}, []);
|
|
4301
|
+
const start = useCallback(() => {
|
|
4302
|
+
if (!isVisible) {
|
|
4303
|
+
setIsVisible(true);
|
|
4304
|
+
}
|
|
4305
|
+
}, [isVisible]);
|
|
4306
|
+
const stop = useCallback(() => {
|
|
4307
|
+
setIsAnimating(false);
|
|
4308
|
+
if (animationRef.current) {
|
|
4309
|
+
cancelAnimationFrame(animationRef.current);
|
|
4310
|
+
animationRef.current = null;
|
|
4311
|
+
}
|
|
4312
|
+
onStop?.();
|
|
4313
|
+
}, [onStop]);
|
|
4314
|
+
const reset = useCallback(() => {
|
|
4315
|
+
setIsVisible(false);
|
|
4316
|
+
setIsAnimating(false);
|
|
4317
|
+
setProgress(0);
|
|
4318
|
+
setIsPressed(false);
|
|
4319
|
+
setIsHovered(false);
|
|
4320
|
+
setIsFocused(false);
|
|
4321
|
+
setRipplePosition({ x: 0, y: 0 });
|
|
4322
|
+
setGlowIntensity(0);
|
|
4323
|
+
setShakeOffset(0);
|
|
4324
|
+
setBounceOffset(0);
|
|
4325
|
+
setSlideOffset(0);
|
|
4326
|
+
startTimeRef.current = 0;
|
|
4327
|
+
if (animationRef.current) {
|
|
4328
|
+
cancelAnimationFrame(animationRef.current);
|
|
4329
|
+
animationRef.current = null;
|
|
4330
|
+
}
|
|
4331
|
+
onReset?.();
|
|
4332
|
+
}, [onReset]);
|
|
4333
|
+
const pause = useCallback(() => {
|
|
4334
|
+
setIsAnimating(false);
|
|
4335
|
+
if (animationRef.current) {
|
|
4336
|
+
cancelAnimationFrame(animationRef.current);
|
|
4337
|
+
animationRef.current = null;
|
|
4338
|
+
}
|
|
4339
|
+
}, []);
|
|
4340
|
+
const resume = useCallback(() => {
|
|
4341
|
+
if (isVisible && !isAnimating) {
|
|
4342
|
+
setIsAnimating(true);
|
|
4343
|
+
}
|
|
4344
|
+
}, [isVisible, isAnimating]);
|
|
4345
|
+
useEffect(() => {
|
|
4346
|
+
if (autoStart) {
|
|
4347
|
+
start();
|
|
4348
|
+
}
|
|
4349
|
+
}, [autoStart, start]);
|
|
4350
|
+
useEffect(() => {
|
|
4351
|
+
return () => {
|
|
4352
|
+
if (animationRef.current) {
|
|
4353
|
+
cancelAnimationFrame(animationRef.current);
|
|
4354
|
+
}
|
|
4355
|
+
};
|
|
4356
|
+
}, []);
|
|
4357
|
+
const getButtonStyle = () => {
|
|
4358
|
+
let scale = 1;
|
|
4359
|
+
let rotate = 0;
|
|
4360
|
+
let translateY = 0;
|
|
4361
|
+
let translateX = 0;
|
|
4362
|
+
let boxShadow = "none";
|
|
4363
|
+
if (isPressed) {
|
|
4364
|
+
scale = activeScale;
|
|
4365
|
+
rotate = activeRotate;
|
|
4366
|
+
translateY = activeTranslateY;
|
|
4367
|
+
translateX = activeTranslateX;
|
|
4368
|
+
} else if (isHovered) {
|
|
4369
|
+
scale = hoverScale;
|
|
4370
|
+
rotate = hoverRotate;
|
|
4371
|
+
translateY = hoverTranslateY;
|
|
4372
|
+
translateX = hoverTranslateX;
|
|
4373
|
+
}
|
|
4374
|
+
if (isFocused && focusGlow) {
|
|
4375
|
+
boxShadow = `0 0 ${glowSize}px ${glowColor}`;
|
|
4376
|
+
}
|
|
4377
|
+
switch (type) {
|
|
4378
|
+
case "shake":
|
|
4379
|
+
translateX += shakeOffset;
|
|
4380
|
+
break;
|
|
4381
|
+
case "bounce":
|
|
4382
|
+
translateY += bounceOffset;
|
|
4383
|
+
break;
|
|
4384
|
+
case "slide":
|
|
4385
|
+
if (slideDirection === "left") translateX -= slideOffset;
|
|
4386
|
+
else if (slideDirection === "right") translateX += slideOffset;
|
|
4387
|
+
else if (slideDirection === "up") translateY -= slideOffset;
|
|
4388
|
+
else if (slideDirection === "down") translateY += slideOffset;
|
|
4389
|
+
break;
|
|
4390
|
+
}
|
|
4391
|
+
const baseStyle = {
|
|
4392
|
+
transform: `
|
|
4393
|
+
scale(${scale})
|
|
4394
|
+
rotate(${rotate}deg)
|
|
4395
|
+
translate(${translateX}px, ${translateY}px)
|
|
4396
|
+
`,
|
|
4397
|
+
boxShadow,
|
|
4398
|
+
opacity: disabled ? disabledOpacity : 1,
|
|
4399
|
+
transition: `all ${duration}ms ${easing2}`,
|
|
4400
|
+
willChange: "transform, box-shadow, opacity",
|
|
4401
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
4402
|
+
position: "relative",
|
|
4403
|
+
overflow: "hidden"
|
|
4404
|
+
};
|
|
4405
|
+
return baseStyle;
|
|
4406
|
+
};
|
|
4407
|
+
const style = getButtonStyle();
|
|
4408
|
+
return {
|
|
4409
|
+
ref,
|
|
4410
|
+
isVisible,
|
|
4411
|
+
isAnimating,
|
|
4412
|
+
style,
|
|
4413
|
+
progress,
|
|
4414
|
+
start,
|
|
4415
|
+
stop,
|
|
4416
|
+
reset,
|
|
4417
|
+
pause,
|
|
4418
|
+
resume,
|
|
4419
|
+
buttonType: type,
|
|
4420
|
+
isPressed,
|
|
4421
|
+
isHovered,
|
|
4422
|
+
isFocused,
|
|
4423
|
+
ripplePosition,
|
|
4424
|
+
currentGlowIntensity,
|
|
4425
|
+
shakeOffset,
|
|
4426
|
+
bounceOffset,
|
|
4427
|
+
slideOffset,
|
|
4428
|
+
pressButton,
|
|
4429
|
+
releaseButton,
|
|
4430
|
+
setButtonState
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
function useVisibilityToggle(options = {}) {
|
|
4434
|
+
const {
|
|
4435
|
+
duration = 300,
|
|
4436
|
+
easing: easing2 = "ease-out",
|
|
4437
|
+
showScale = 1,
|
|
4438
|
+
showOpacity = 1,
|
|
4439
|
+
showRotate = 0,
|
|
4440
|
+
showTranslateY = 0,
|
|
4441
|
+
showTranslateX = 0,
|
|
4442
|
+
hideScale = 0.8,
|
|
4443
|
+
hideOpacity = 0,
|
|
4444
|
+
hideRotate = 0,
|
|
4445
|
+
hideTranslateY = 20,
|
|
4446
|
+
hideTranslateX = 0,
|
|
4447
|
+
onComplete,
|
|
4448
|
+
onStart,
|
|
4449
|
+
onStop,
|
|
4450
|
+
onReset
|
|
4451
|
+
} = options;
|
|
4452
|
+
const ref = useRef(null);
|
|
4453
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
4454
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
4455
|
+
const [progress, setProgress] = useState(0);
|
|
4456
|
+
const toggle = useCallback(() => {
|
|
4457
|
+
setIsAnimating(true);
|
|
4458
|
+
setProgress(0);
|
|
4459
|
+
onStart?.();
|
|
4460
|
+
const newVisibility = !isVisible;
|
|
4461
|
+
setIsVisible(newVisibility);
|
|
4462
|
+
setTimeout(() => {
|
|
4463
|
+
setIsAnimating(false);
|
|
4464
|
+
setProgress(1);
|
|
4465
|
+
onComplete?.();
|
|
4466
|
+
}, duration);
|
|
4467
|
+
}, [isVisible, duration, onStart, onComplete]);
|
|
4468
|
+
const show = useCallback(() => {
|
|
4469
|
+
if (!isVisible) {
|
|
4470
|
+
setIsAnimating(true);
|
|
4471
|
+
setProgress(0);
|
|
4472
|
+
onStart?.();
|
|
4473
|
+
setIsVisible(true);
|
|
4474
|
+
setTimeout(() => {
|
|
4475
|
+
setIsAnimating(false);
|
|
4476
|
+
setProgress(1);
|
|
4477
|
+
onComplete?.();
|
|
4478
|
+
}, duration);
|
|
4479
|
+
}
|
|
4480
|
+
}, [isVisible, duration, onStart, onComplete]);
|
|
4481
|
+
const hide = useCallback(() => {
|
|
4482
|
+
if (isVisible) {
|
|
4483
|
+
setIsAnimating(true);
|
|
4484
|
+
setProgress(1);
|
|
4485
|
+
onStart?.();
|
|
4486
|
+
setIsVisible(false);
|
|
4487
|
+
setTimeout(() => {
|
|
4488
|
+
setIsAnimating(false);
|
|
4489
|
+
setProgress(0);
|
|
4490
|
+
onComplete?.();
|
|
4491
|
+
}, duration);
|
|
4492
|
+
}
|
|
4493
|
+
}, [isVisible, duration, onStart, onComplete]);
|
|
4494
|
+
const start = useCallback(() => {
|
|
4495
|
+
if (!isVisible) {
|
|
4496
|
+
toggle();
|
|
4497
|
+
}
|
|
4498
|
+
}, [isVisible, toggle]);
|
|
4499
|
+
const stop = useCallback(() => {
|
|
4500
|
+
setIsAnimating(false);
|
|
4501
|
+
onStop?.();
|
|
4502
|
+
}, [onStop]);
|
|
4503
|
+
const reset = useCallback(() => {
|
|
4504
|
+
setIsVisible(false);
|
|
4505
|
+
setIsAnimating(false);
|
|
4506
|
+
setProgress(0);
|
|
4507
|
+
onReset?.();
|
|
4508
|
+
}, [onReset]);
|
|
4509
|
+
const pause = useCallback(() => {
|
|
4510
|
+
setIsAnimating(false);
|
|
4511
|
+
}, []);
|
|
4512
|
+
const resume = useCallback(() => {
|
|
4513
|
+
if (isVisible) {
|
|
4514
|
+
setIsAnimating(true);
|
|
4515
|
+
}
|
|
4516
|
+
}, [isVisible]);
|
|
4517
|
+
const style = {
|
|
4518
|
+
transform: `
|
|
4519
|
+
scale(${isVisible ? showScale : hideScale})
|
|
4520
|
+
rotate(${isVisible ? showRotate : hideRotate}deg)
|
|
4521
|
+
translate(${isVisible ? showTranslateX : hideTranslateX}px, ${isVisible ? showTranslateY : hideTranslateY}px)
|
|
4522
|
+
`,
|
|
4523
|
+
opacity: isVisible ? showOpacity : hideOpacity,
|
|
4524
|
+
transition: `all ${duration}ms ${easing2}`,
|
|
4525
|
+
willChange: "transform, opacity"
|
|
4526
|
+
};
|
|
4527
|
+
return {
|
|
4528
|
+
ref,
|
|
4529
|
+
isVisible,
|
|
4530
|
+
isAnimating,
|
|
4531
|
+
style,
|
|
4532
|
+
progress,
|
|
4533
|
+
start,
|
|
4534
|
+
stop,
|
|
4535
|
+
reset,
|
|
4536
|
+
pause,
|
|
4537
|
+
resume,
|
|
4538
|
+
// 추가 메서드
|
|
4539
|
+
toggle,
|
|
4540
|
+
show,
|
|
4541
|
+
hide
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4544
|
+
function useScrollToggle(options = {}) {
|
|
4545
|
+
const {
|
|
4546
|
+
duration = 300,
|
|
4547
|
+
easing: easing2 = "ease-out",
|
|
4548
|
+
showScale = 1,
|
|
4549
|
+
showOpacity = 1,
|
|
4550
|
+
showRotate = 0,
|
|
4551
|
+
showTranslateY = 0,
|
|
4552
|
+
showTranslateX = 0,
|
|
4553
|
+
hideScale = 0.8,
|
|
4554
|
+
hideOpacity = 0,
|
|
4555
|
+
hideRotate = 0,
|
|
4556
|
+
hideTranslateY = 20,
|
|
4557
|
+
hideTranslateX = 0,
|
|
4558
|
+
scrollThreshold = 0.1,
|
|
4559
|
+
scrollDirection = "both",
|
|
4560
|
+
onComplete,
|
|
4561
|
+
onStart,
|
|
4562
|
+
onStop,
|
|
4563
|
+
onReset
|
|
4564
|
+
} = options;
|
|
4565
|
+
const ref = useRef(null);
|
|
4566
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
4567
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
4568
|
+
const [progress, setProgress] = useState(0);
|
|
4569
|
+
const isVisibleRef = useRef(false);
|
|
4570
|
+
const lastScrollYRef = useRef(0);
|
|
4571
|
+
useEffect(() => {
|
|
4572
|
+
isVisibleRef.current = isVisible;
|
|
4573
|
+
}, [isVisible]);
|
|
4574
|
+
const handleScroll = useCallback(() => {
|
|
4575
|
+
if (!ref.current) return;
|
|
4576
|
+
const currentScrollY = window.scrollY;
|
|
4577
|
+
const rect = ref.current.getBoundingClientRect();
|
|
4578
|
+
const threshold = window.innerHeight * scrollThreshold;
|
|
4579
|
+
const isScrollingDown = currentScrollY > lastScrollYRef.current;
|
|
4580
|
+
const isScrollingUp = currentScrollY < lastScrollYRef.current;
|
|
4581
|
+
let shouldToggle = false;
|
|
4582
|
+
if (scrollDirection === "both") {
|
|
4583
|
+
shouldToggle = rect.top <= threshold;
|
|
4584
|
+
} else if (scrollDirection === "down" && isScrollingDown) {
|
|
4585
|
+
shouldToggle = rect.top <= threshold;
|
|
4586
|
+
} else if (scrollDirection === "up" && isScrollingUp) {
|
|
4587
|
+
shouldToggle = rect.top <= threshold;
|
|
4588
|
+
}
|
|
4589
|
+
if (shouldToggle && !isVisibleRef.current) {
|
|
4590
|
+
isVisibleRef.current = true;
|
|
4591
|
+
setIsVisible(true);
|
|
4592
|
+
setIsAnimating(true);
|
|
4593
|
+
setProgress(0);
|
|
4594
|
+
onStart?.();
|
|
4595
|
+
setTimeout(() => {
|
|
4596
|
+
setIsAnimating(false);
|
|
4597
|
+
setProgress(1);
|
|
4598
|
+
onComplete?.();
|
|
4599
|
+
}, duration);
|
|
4600
|
+
} else if (!shouldToggle && isVisibleRef.current) {
|
|
4601
|
+
isVisibleRef.current = false;
|
|
4602
|
+
setIsVisible(false);
|
|
4603
|
+
setIsAnimating(true);
|
|
4604
|
+
setProgress(1);
|
|
4605
|
+
setTimeout(() => {
|
|
4606
|
+
setIsAnimating(false);
|
|
4607
|
+
setProgress(0);
|
|
4608
|
+
}, duration);
|
|
4609
|
+
}
|
|
4610
|
+
lastScrollYRef.current = currentScrollY;
|
|
4611
|
+
}, [scrollDirection, scrollThreshold, duration, onStart, onComplete]);
|
|
4612
|
+
useEffect(() => {
|
|
4613
|
+
handleScroll();
|
|
4614
|
+
return subscribeScroll(handleScroll);
|
|
4615
|
+
}, [handleScroll]);
|
|
4616
|
+
const start = useCallback(() => {
|
|
4617
|
+
if (!isVisibleRef.current) {
|
|
4618
|
+
isVisibleRef.current = true;
|
|
4619
|
+
setIsVisible(true);
|
|
4620
|
+
setIsAnimating(true);
|
|
4621
|
+
setProgress(0);
|
|
4622
|
+
onStart?.();
|
|
4623
|
+
setTimeout(() => {
|
|
4624
|
+
setIsAnimating(false);
|
|
4625
|
+
setProgress(1);
|
|
4626
|
+
onComplete?.();
|
|
4627
|
+
}, duration);
|
|
4628
|
+
}
|
|
4629
|
+
}, [duration, onStart, onComplete]);
|
|
4630
|
+
const stop = useCallback(() => {
|
|
4631
|
+
setIsAnimating(false);
|
|
4632
|
+
onStop?.();
|
|
4633
|
+
}, [onStop]);
|
|
4634
|
+
const reset = useCallback(() => {
|
|
4635
|
+
isVisibleRef.current = false;
|
|
4636
|
+
setIsVisible(false);
|
|
4637
|
+
setIsAnimating(false);
|
|
4638
|
+
setProgress(0);
|
|
4639
|
+
onReset?.();
|
|
4640
|
+
}, [onReset]);
|
|
4641
|
+
const pause = useCallback(() => {
|
|
4642
|
+
setIsAnimating(false);
|
|
4643
|
+
}, []);
|
|
4644
|
+
const resume = useCallback(() => {
|
|
4645
|
+
if (isVisibleRef.current) {
|
|
4646
|
+
setIsAnimating(true);
|
|
4647
|
+
}
|
|
4648
|
+
}, []);
|
|
4649
|
+
const style = {
|
|
4650
|
+
transform: `
|
|
4651
|
+
scale(${isVisible ? showScale : hideScale})
|
|
4652
|
+
rotate(${isVisible ? showRotate : hideRotate}deg)
|
|
4653
|
+
translate(${isVisible ? showTranslateX : hideTranslateX}px, ${isVisible ? showTranslateY : hideTranslateY}px)
|
|
4654
|
+
`,
|
|
4655
|
+
opacity: isVisible ? showOpacity : hideOpacity,
|
|
4656
|
+
transition: `all ${duration}ms ${easing2}`,
|
|
4657
|
+
willChange: "transform, opacity"
|
|
4658
|
+
};
|
|
4659
|
+
return {
|
|
4660
|
+
ref,
|
|
4661
|
+
isVisible,
|
|
4662
|
+
isAnimating,
|
|
4663
|
+
style,
|
|
4664
|
+
progress,
|
|
4665
|
+
start,
|
|
4666
|
+
stop,
|
|
4667
|
+
reset,
|
|
4668
|
+
pause,
|
|
4669
|
+
resume
|
|
4670
|
+
};
|
|
4671
|
+
}
|
|
4672
|
+
function useCardList(options = {}) {
|
|
4673
|
+
const {
|
|
4674
|
+
duration = 500,
|
|
4675
|
+
easing: easing2 = "ease-out",
|
|
4676
|
+
staggerDelay = 100,
|
|
4677
|
+
cardScale = 1,
|
|
4678
|
+
cardOpacity = 1,
|
|
4679
|
+
cardRotate = 0,
|
|
4680
|
+
cardTranslateY = 0,
|
|
4681
|
+
cardTranslateX = 0,
|
|
4682
|
+
initialScale = 0.8,
|
|
4683
|
+
initialOpacity = 0,
|
|
4684
|
+
initialRotate = 0,
|
|
4685
|
+
initialTranslateY = 30,
|
|
4686
|
+
initialTranslateX = 0,
|
|
4687
|
+
gridColumns = 3,
|
|
4688
|
+
gridGap = 20,
|
|
4689
|
+
onComplete,
|
|
4690
|
+
onStart,
|
|
4691
|
+
onStop,
|
|
4692
|
+
onReset
|
|
4693
|
+
} = options;
|
|
4694
|
+
const ref = useRef(null);
|
|
4695
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
4696
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
4697
|
+
const [progress, setProgress] = useState(0);
|
|
4698
|
+
const [cardCount, setCardCount] = useState(0);
|
|
4699
|
+
const startRef = useRef(() => {
|
|
4700
|
+
});
|
|
4701
|
+
useEffect(() => {
|
|
4702
|
+
if (ref.current) {
|
|
4703
|
+
const cards = ref.current.querySelectorAll("[data-card]");
|
|
4704
|
+
setCardCount(cards.length);
|
|
4705
|
+
}
|
|
4706
|
+
}, []);
|
|
4707
|
+
const cardStyles = Array.from({ length: cardCount }, (_, index) => {
|
|
4708
|
+
const delay = isVisible ? index * staggerDelay : 0;
|
|
4709
|
+
const isCardVisible = isVisible && delay <= progress * (cardCount * staggerDelay);
|
|
4710
|
+
return {
|
|
4711
|
+
transform: `
|
|
4712
|
+
scale(${isCardVisible ? cardScale : initialScale})
|
|
4713
|
+
rotate(${isCardVisible ? cardRotate : initialRotate}deg)
|
|
4714
|
+
translate(${isCardVisible ? cardTranslateX : initialTranslateX}px, ${isCardVisible ? cardTranslateY : initialTranslateY}px)
|
|
4715
|
+
`,
|
|
4716
|
+
opacity: isCardVisible ? cardOpacity : initialOpacity,
|
|
4717
|
+
transition: `all ${duration}ms ${easing2} ${delay}ms`,
|
|
4718
|
+
willChange: "transform, opacity"
|
|
4719
|
+
};
|
|
4720
|
+
});
|
|
4721
|
+
const start = useCallback(() => {
|
|
4722
|
+
if (isAnimating) return;
|
|
4723
|
+
setIsAnimating(true);
|
|
4724
|
+
setProgress(0);
|
|
4725
|
+
onStart?.();
|
|
4726
|
+
const totalDuration = cardCount * staggerDelay + duration;
|
|
4727
|
+
const interval = setInterval(() => {
|
|
4728
|
+
setProgress((prev) => {
|
|
4729
|
+
const newProgress = prev + 0.1;
|
|
4730
|
+
if (newProgress >= 1) {
|
|
4731
|
+
setIsVisible(true);
|
|
4732
|
+
setIsAnimating(false);
|
|
4733
|
+
onComplete?.();
|
|
4734
|
+
clearInterval(interval);
|
|
4735
|
+
return 1;
|
|
4736
|
+
}
|
|
4737
|
+
return newProgress;
|
|
4738
|
+
});
|
|
4739
|
+
}, totalDuration / 10);
|
|
4740
|
+
}, [isAnimating, cardCount, staggerDelay, duration, onStart, onComplete]);
|
|
4741
|
+
startRef.current = start;
|
|
4742
|
+
const stop = useCallback(() => {
|
|
4743
|
+
setIsAnimating(false);
|
|
4744
|
+
onStop?.();
|
|
4745
|
+
}, [onStop]);
|
|
4746
|
+
const reset = useCallback(() => {
|
|
4747
|
+
setIsVisible(false);
|
|
4748
|
+
setIsAnimating(false);
|
|
4749
|
+
setProgress(0);
|
|
4750
|
+
onReset?.();
|
|
4751
|
+
}, [onReset]);
|
|
4752
|
+
const pause = useCallback(() => {
|
|
4753
|
+
setIsAnimating(false);
|
|
4754
|
+
}, []);
|
|
4755
|
+
const resume = useCallback(() => {
|
|
4756
|
+
if (!isVisible && !isAnimating) {
|
|
4757
|
+
start();
|
|
4758
|
+
}
|
|
4759
|
+
}, [isVisible, isAnimating, start]);
|
|
4760
|
+
useEffect(() => {
|
|
4761
|
+
if (!ref.current) return;
|
|
4762
|
+
return observeElement(
|
|
4763
|
+
ref.current,
|
|
4764
|
+
(entry) => {
|
|
4765
|
+
if (entry.isIntersecting) startRef.current();
|
|
4766
|
+
},
|
|
4767
|
+
{ threshold: 0.1 }
|
|
4768
|
+
);
|
|
4769
|
+
}, []);
|
|
4770
|
+
const gridStyle = {
|
|
4771
|
+
display: "grid",
|
|
4772
|
+
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
|
4773
|
+
gap: `${gridGap}px`,
|
|
4774
|
+
width: "100%"
|
|
4775
|
+
};
|
|
4776
|
+
return {
|
|
4777
|
+
ref,
|
|
4778
|
+
isVisible,
|
|
4779
|
+
isAnimating,
|
|
4780
|
+
style: gridStyle,
|
|
4781
|
+
progress,
|
|
4782
|
+
start,
|
|
4783
|
+
stop,
|
|
4784
|
+
reset,
|
|
4785
|
+
pause,
|
|
4786
|
+
resume,
|
|
4787
|
+
cardStyles,
|
|
4788
|
+
staggerDelay,
|
|
4789
|
+
gridColumns,
|
|
4790
|
+
gridGap
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
function useLoadingSpinner(options = {}) {
|
|
4794
|
+
const {
|
|
4795
|
+
duration = 1e3,
|
|
4796
|
+
easing: easing2 = "linear",
|
|
4797
|
+
type = "rotate",
|
|
4798
|
+
rotationSpeed = 1,
|
|
4799
|
+
pulseSpeed = 1,
|
|
4800
|
+
bounceHeight = 20,
|
|
4801
|
+
waveCount = 3,
|
|
4802
|
+
dotCount = 3,
|
|
4803
|
+
barCount = 4,
|
|
4804
|
+
color = "#3b82f6",
|
|
4805
|
+
backgroundColor = "transparent",
|
|
4806
|
+
size = 40,
|
|
4807
|
+
thickness = 4,
|
|
4808
|
+
autoStart = true,
|
|
4809
|
+
infinite = true,
|
|
4810
|
+
onComplete,
|
|
4811
|
+
onStart,
|
|
4812
|
+
onStop,
|
|
4813
|
+
onReset
|
|
4814
|
+
} = options;
|
|
4815
|
+
const ref = useRef(null);
|
|
4816
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
4817
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
4818
|
+
const [progress, setProgress] = useState(0);
|
|
4819
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
4820
|
+
const [rotationAngle, setRotationAngle] = useState(0);
|
|
4821
|
+
const [pulseScale, setPulseScale] = useState(1);
|
|
4822
|
+
const [bounceOffset, setBounceOffset] = useState(0);
|
|
4823
|
+
const [waveProgress, setWaveProgress] = useState(0);
|
|
4824
|
+
const [dotProgress, setDotProgress] = useState(0);
|
|
4825
|
+
const [barProgress, setBarProgress] = useState(0);
|
|
4826
|
+
const animationRef = useRef(null);
|
|
4827
|
+
const startTimeRef = useRef(0);
|
|
4828
|
+
const runAnimation = useCallback(() => {
|
|
4829
|
+
if (!isAnimating) return;
|
|
4830
|
+
const animate = (currentTime) => {
|
|
4831
|
+
if (!isAnimating) return;
|
|
4832
|
+
if (!startTimeRef.current) {
|
|
4833
|
+
startTimeRef.current = currentTime;
|
|
4834
|
+
}
|
|
4835
|
+
const elapsed = currentTime - startTimeRef.current;
|
|
4836
|
+
const currentProgress = elapsed / duration % 1;
|
|
4837
|
+
setProgress(currentProgress);
|
|
4838
|
+
switch (type) {
|
|
4839
|
+
case "rotate":
|
|
4840
|
+
setRotationAngle(currentProgress * 360 * rotationSpeed);
|
|
4841
|
+
break;
|
|
4842
|
+
case "pulse":
|
|
4843
|
+
setPulseScale(0.8 + 0.4 * Math.sin(currentProgress * Math.PI * 2 * pulseSpeed));
|
|
4844
|
+
break;
|
|
4845
|
+
case "bounce":
|
|
4846
|
+
setBounceOffset(bounceHeight * Math.sin(currentProgress * Math.PI * 2));
|
|
4847
|
+
break;
|
|
4848
|
+
case "wave":
|
|
4849
|
+
setWaveProgress(currentProgress);
|
|
4850
|
+
break;
|
|
4851
|
+
case "dots":
|
|
4852
|
+
setDotProgress(currentProgress);
|
|
4853
|
+
break;
|
|
4854
|
+
case "bars":
|
|
4855
|
+
setBarProgress(currentProgress);
|
|
4856
|
+
break;
|
|
4857
|
+
}
|
|
4858
|
+
if (infinite || currentProgress < 1) {
|
|
4859
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
4860
|
+
} else {
|
|
4861
|
+
setIsAnimating(false);
|
|
4862
|
+
onComplete?.();
|
|
4863
|
+
}
|
|
4864
|
+
};
|
|
4865
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
4866
|
+
}, [isAnimating, duration, type, rotationSpeed, pulseSpeed, bounceHeight, infinite, onComplete]);
|
|
4867
|
+
const startLoading = useCallback(() => {
|
|
4868
|
+
if (isLoading) return;
|
|
4869
|
+
setIsLoading(true);
|
|
4870
|
+
setIsAnimating(true);
|
|
4871
|
+
setProgress(0);
|
|
4872
|
+
startTimeRef.current = 0;
|
|
4873
|
+
onStart?.();
|
|
4874
|
+
runAnimation();
|
|
4875
|
+
}, [isLoading, onStart, runAnimation]);
|
|
4876
|
+
const stopLoading = useCallback(() => {
|
|
4877
|
+
setIsLoading(false);
|
|
4878
|
+
setIsAnimating(false);
|
|
4879
|
+
if (animationRef.current) {
|
|
4880
|
+
cancelAnimationFrame(animationRef.current);
|
|
4881
|
+
animationRef.current = null;
|
|
4882
|
+
}
|
|
4883
|
+
onStop?.();
|
|
4884
|
+
}, [onStop]);
|
|
4885
|
+
const setLoadingState = useCallback((loading) => {
|
|
4886
|
+
if (loading) {
|
|
4887
|
+
startLoading();
|
|
4888
|
+
} else {
|
|
4889
|
+
stopLoading();
|
|
4890
|
+
}
|
|
4891
|
+
}, [startLoading, stopLoading]);
|
|
4892
|
+
const start = useCallback(() => {
|
|
4893
|
+
if (!isVisible) {
|
|
4894
|
+
setIsVisible(true);
|
|
4895
|
+
startLoading();
|
|
4896
|
+
}
|
|
4897
|
+
}, [isVisible, startLoading]);
|
|
4898
|
+
const stop = useCallback(() => {
|
|
4899
|
+
stopLoading();
|
|
4900
|
+
}, [stopLoading]);
|
|
4901
|
+
const reset = useCallback(() => {
|
|
4902
|
+
setIsVisible(false);
|
|
4903
|
+
setIsAnimating(false);
|
|
4904
|
+
setProgress(0);
|
|
4905
|
+
setIsLoading(false);
|
|
4906
|
+
setRotationAngle(0);
|
|
4907
|
+
setPulseScale(1);
|
|
4908
|
+
setBounceOffset(0);
|
|
4909
|
+
setWaveProgress(0);
|
|
4910
|
+
setDotProgress(0);
|
|
4911
|
+
setBarProgress(0);
|
|
4912
|
+
startTimeRef.current = 0;
|
|
4913
|
+
if (animationRef.current) {
|
|
4914
|
+
cancelAnimationFrame(animationRef.current);
|
|
4915
|
+
animationRef.current = null;
|
|
4916
|
+
}
|
|
4917
|
+
onReset?.();
|
|
4918
|
+
}, [onReset]);
|
|
4919
|
+
const pause = useCallback(() => {
|
|
4920
|
+
setIsAnimating(false);
|
|
4921
|
+
if (animationRef.current) {
|
|
4922
|
+
cancelAnimationFrame(animationRef.current);
|
|
4923
|
+
animationRef.current = null;
|
|
4924
|
+
}
|
|
4925
|
+
}, []);
|
|
4926
|
+
const resume = useCallback(() => {
|
|
4927
|
+
if (isVisible && !isAnimating) {
|
|
4928
|
+
setIsAnimating(true);
|
|
4929
|
+
runAnimation();
|
|
4930
|
+
}
|
|
4931
|
+
}, [isVisible, isAnimating, runAnimation]);
|
|
4932
|
+
useEffect(() => {
|
|
4933
|
+
if (autoStart) {
|
|
4934
|
+
startLoading();
|
|
4935
|
+
}
|
|
4936
|
+
}, [autoStart, startLoading]);
|
|
4937
|
+
useEffect(() => {
|
|
4938
|
+
return () => {
|
|
4939
|
+
if (animationRef.current) {
|
|
4940
|
+
cancelAnimationFrame(animationRef.current);
|
|
4941
|
+
}
|
|
4942
|
+
};
|
|
4943
|
+
}, []);
|
|
4944
|
+
const getSpinnerStyle = () => {
|
|
4945
|
+
const baseStyle = {
|
|
4946
|
+
width: size,
|
|
4947
|
+
height: size,
|
|
4948
|
+
backgroundColor: "transparent",
|
|
4949
|
+
position: "relative",
|
|
4950
|
+
display: "inline-block"
|
|
4951
|
+
};
|
|
4952
|
+
switch (type) {
|
|
4953
|
+
case "rotate":
|
|
4954
|
+
return {
|
|
4955
|
+
...baseStyle,
|
|
4956
|
+
border: `${thickness}px solid ${backgroundColor}`,
|
|
4957
|
+
borderTop: `${thickness}px solid ${color}`,
|
|
4958
|
+
borderRadius: "50%",
|
|
4959
|
+
transform: `rotate(${rotationAngle}deg)`,
|
|
4960
|
+
transition: `transform ${duration}ms ${easing2}`
|
|
4961
|
+
};
|
|
4962
|
+
case "pulse":
|
|
4963
|
+
return {
|
|
4964
|
+
...baseStyle,
|
|
4965
|
+
backgroundColor: color,
|
|
4966
|
+
borderRadius: "50%",
|
|
4967
|
+
transform: `scale(${pulseScale})`,
|
|
4968
|
+
transition: `transform ${duration}ms ${easing2}`
|
|
4969
|
+
};
|
|
4970
|
+
case "bounce":
|
|
4971
|
+
return {
|
|
4972
|
+
...baseStyle,
|
|
4973
|
+
backgroundColor: color,
|
|
4974
|
+
borderRadius: "50%",
|
|
4975
|
+
transform: `translateY(${bounceOffset}px)`,
|
|
4976
|
+
transition: `transform ${duration}ms ${easing2}`
|
|
4977
|
+
};
|
|
4978
|
+
case "wave":
|
|
4979
|
+
return {
|
|
4980
|
+
...baseStyle,
|
|
4981
|
+
display: "flex",
|
|
4982
|
+
alignItems: "center",
|
|
4983
|
+
justifyContent: "space-between"
|
|
4984
|
+
};
|
|
4985
|
+
case "dots":
|
|
4986
|
+
return {
|
|
4987
|
+
...baseStyle,
|
|
4988
|
+
display: "flex",
|
|
4989
|
+
alignItems: "center",
|
|
4990
|
+
justifyContent: "space-between"
|
|
4991
|
+
};
|
|
4992
|
+
case "bars":
|
|
4993
|
+
return {
|
|
4994
|
+
...baseStyle,
|
|
4995
|
+
display: "flex",
|
|
4996
|
+
alignItems: "center",
|
|
4997
|
+
justifyContent: "space-between"
|
|
4998
|
+
};
|
|
4999
|
+
default:
|
|
5000
|
+
return baseStyle;
|
|
5001
|
+
}
|
|
5002
|
+
};
|
|
5003
|
+
const style = getSpinnerStyle();
|
|
5004
|
+
return {
|
|
5005
|
+
ref,
|
|
5006
|
+
isVisible,
|
|
5007
|
+
isAnimating,
|
|
5008
|
+
style,
|
|
5009
|
+
progress,
|
|
5010
|
+
start,
|
|
5011
|
+
stop,
|
|
5012
|
+
reset,
|
|
5013
|
+
pause,
|
|
5014
|
+
resume,
|
|
5015
|
+
isLoading,
|
|
5016
|
+
spinnerType: type,
|
|
5017
|
+
rotationAngle,
|
|
5018
|
+
pulseScale,
|
|
5019
|
+
bounceOffset,
|
|
5020
|
+
waveProgress,
|
|
5021
|
+
dotProgress,
|
|
5022
|
+
barProgress,
|
|
5023
|
+
startLoading,
|
|
5024
|
+
stopLoading,
|
|
5025
|
+
setLoadingState
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
function useNavigation(options = {}) {
|
|
5029
|
+
const {
|
|
5030
|
+
duration = 300,
|
|
5031
|
+
easing: easing2 = "ease-out",
|
|
5032
|
+
type = "slide",
|
|
5033
|
+
slideDirection = "left",
|
|
5034
|
+
staggerDelay = 50,
|
|
5035
|
+
itemScale = 1,
|
|
5036
|
+
itemOpacity = 1,
|
|
5037
|
+
itemRotate = 0,
|
|
5038
|
+
itemTranslateY = 0,
|
|
5039
|
+
itemTranslateX = 0,
|
|
5040
|
+
initialScale = 0.8,
|
|
5041
|
+
initialOpacity = 0,
|
|
5042
|
+
initialRotate = -10,
|
|
5043
|
+
initialTranslateY = 20,
|
|
5044
|
+
initialTranslateX = 0,
|
|
5045
|
+
activeScale = 1.05,
|
|
5046
|
+
activeOpacity = 1,
|
|
5047
|
+
activeRotate = 0,
|
|
5048
|
+
activeTranslateY = 0,
|
|
5049
|
+
activeTranslateX = 0,
|
|
5050
|
+
hoverScale = 1.1,
|
|
5051
|
+
hoverOpacity = 1,
|
|
5052
|
+
hoverRotate = 5,
|
|
5053
|
+
hoverTranslateY = -5,
|
|
5054
|
+
hoverTranslateX = 0,
|
|
5055
|
+
itemCount = 5,
|
|
5056
|
+
autoStart = false,
|
|
5057
|
+
onComplete,
|
|
5058
|
+
onStart,
|
|
5059
|
+
onStop,
|
|
5060
|
+
onReset
|
|
5061
|
+
} = options;
|
|
5062
|
+
const ref = useRef(null);
|
|
5063
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
5064
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
5065
|
+
const [progress, setProgress] = useState(0);
|
|
5066
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
5067
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
5068
|
+
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
5069
|
+
const openMenu = useCallback(() => {
|
|
5070
|
+
if (isOpen) return;
|
|
5071
|
+
setIsOpen(true);
|
|
5072
|
+
setIsAnimating(true);
|
|
5073
|
+
setProgress(0);
|
|
5074
|
+
onStart?.();
|
|
5075
|
+
const totalDuration = itemCount * staggerDelay + duration;
|
|
5076
|
+
const interval = setInterval(() => {
|
|
5077
|
+
setProgress((prev) => {
|
|
5078
|
+
const newProgress = prev + 0.1;
|
|
5079
|
+
if (newProgress >= 1) {
|
|
5080
|
+
setIsAnimating(false);
|
|
5081
|
+
onComplete?.();
|
|
5082
|
+
clearInterval(interval);
|
|
5083
|
+
return 1;
|
|
5084
|
+
}
|
|
5085
|
+
return newProgress;
|
|
5086
|
+
});
|
|
5087
|
+
}, totalDuration / 10);
|
|
5088
|
+
}, [isOpen, itemCount, staggerDelay, duration, onStart, onComplete]);
|
|
5089
|
+
const closeMenu = useCallback(() => {
|
|
5090
|
+
if (!isOpen) return;
|
|
5091
|
+
setIsOpen(false);
|
|
5092
|
+
setIsAnimating(true);
|
|
5093
|
+
setProgress(1);
|
|
5094
|
+
setTimeout(() => {
|
|
5095
|
+
setIsAnimating(false);
|
|
5096
|
+
setProgress(0);
|
|
5097
|
+
}, duration);
|
|
5098
|
+
}, [isOpen, duration]);
|
|
5099
|
+
const toggleMenu = useCallback(() => {
|
|
5100
|
+
if (isOpen) {
|
|
5101
|
+
closeMenu();
|
|
5102
|
+
} else {
|
|
5103
|
+
openMenu();
|
|
5104
|
+
}
|
|
5105
|
+
}, [isOpen, openMenu, closeMenu]);
|
|
5106
|
+
const setActiveItem = useCallback((index) => {
|
|
5107
|
+
if (index >= 0 && index < itemCount) {
|
|
5108
|
+
setActiveIndex(index);
|
|
5109
|
+
}
|
|
5110
|
+
}, [itemCount]);
|
|
5111
|
+
const goToNext = useCallback(() => {
|
|
5112
|
+
setActiveItem((activeIndex + 1) % itemCount);
|
|
5113
|
+
}, [activeIndex, itemCount, setActiveItem]);
|
|
5114
|
+
const goToPrevious = useCallback(() => {
|
|
5115
|
+
setActiveItem(activeIndex === 0 ? itemCount - 1 : activeIndex - 1);
|
|
5116
|
+
}, [activeIndex, itemCount, setActiveItem]);
|
|
5117
|
+
const start = useCallback(() => {
|
|
5118
|
+
if (!isVisible) {
|
|
5119
|
+
setIsVisible(true);
|
|
5120
|
+
openMenu();
|
|
5121
|
+
}
|
|
5122
|
+
}, [isVisible, openMenu]);
|
|
5123
|
+
const stop = useCallback(() => {
|
|
5124
|
+
setIsAnimating(false);
|
|
5125
|
+
onStop?.();
|
|
5126
|
+
}, [onStop]);
|
|
5127
|
+
const reset = useCallback(() => {
|
|
5128
|
+
setIsVisible(false);
|
|
5129
|
+
setIsAnimating(false);
|
|
5130
|
+
setProgress(0);
|
|
5131
|
+
setIsOpen(false);
|
|
5132
|
+
setActiveIndex(0);
|
|
5133
|
+
setHoveredIndex(null);
|
|
5134
|
+
onReset?.();
|
|
5135
|
+
}, [onReset]);
|
|
5136
|
+
const pause = useCallback(() => {
|
|
5137
|
+
setIsAnimating(false);
|
|
5138
|
+
}, []);
|
|
5139
|
+
const resume = useCallback(() => {
|
|
5140
|
+
if (isVisible) {
|
|
5141
|
+
setIsAnimating(true);
|
|
5142
|
+
}
|
|
5143
|
+
}, [isVisible]);
|
|
5144
|
+
useEffect(() => {
|
|
5145
|
+
if (autoStart) {
|
|
5146
|
+
start();
|
|
5147
|
+
}
|
|
5148
|
+
}, [autoStart, start]);
|
|
5149
|
+
const itemStyles = Array.from({ length: itemCount }, (_, index) => {
|
|
5150
|
+
const delay = isOpen ? index * staggerDelay : 0;
|
|
5151
|
+
const isItemVisible = isOpen && delay <= progress * (itemCount * staggerDelay);
|
|
5152
|
+
const isActive = index === activeIndex;
|
|
5153
|
+
const isHovered = index === hoveredIndex;
|
|
5154
|
+
let scale = initialScale;
|
|
5155
|
+
let opacity = initialOpacity;
|
|
5156
|
+
let rotate = initialRotate;
|
|
5157
|
+
let translateY = initialTranslateY;
|
|
5158
|
+
let translateX = initialTranslateX;
|
|
5159
|
+
if (isItemVisible) {
|
|
5160
|
+
if (isHovered) {
|
|
5161
|
+
scale = hoverScale;
|
|
5162
|
+
opacity = hoverOpacity;
|
|
5163
|
+
rotate = hoverRotate;
|
|
5164
|
+
translateY = hoverTranslateY;
|
|
5165
|
+
translateX = hoverTranslateX;
|
|
5166
|
+
} else if (isActive) {
|
|
5167
|
+
scale = activeScale;
|
|
5168
|
+
opacity = activeOpacity;
|
|
5169
|
+
rotate = activeRotate;
|
|
5170
|
+
translateY = activeTranslateY;
|
|
5171
|
+
translateX = activeTranslateX;
|
|
5172
|
+
} else {
|
|
5173
|
+
scale = itemScale;
|
|
5174
|
+
opacity = itemOpacity;
|
|
5175
|
+
rotate = itemRotate;
|
|
5176
|
+
translateY = itemTranslateY;
|
|
5177
|
+
translateX = itemTranslateX;
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
return {
|
|
5181
|
+
transform: `
|
|
5182
|
+
scale(${scale})
|
|
5183
|
+
rotate(${rotate}deg)
|
|
5184
|
+
translate(${translateX}px, ${translateY}px)
|
|
5185
|
+
`,
|
|
5186
|
+
opacity,
|
|
5187
|
+
transition: `all ${duration}ms ${easing2} ${delay}ms`,
|
|
5188
|
+
willChange: "transform, opacity",
|
|
5189
|
+
cursor: "pointer"
|
|
5190
|
+
};
|
|
5191
|
+
});
|
|
5192
|
+
const getNavigationStyle = () => {
|
|
5193
|
+
const baseStyle = {
|
|
5194
|
+
transition: `all ${duration}ms ${easing2}`,
|
|
5195
|
+
willChange: "transform, opacity"
|
|
5196
|
+
};
|
|
5197
|
+
switch (type) {
|
|
5198
|
+
case "slide":
|
|
5199
|
+
if (slideDirection === "left") {
|
|
5200
|
+
baseStyle.transform = `translateX(${isOpen ? 0 : -100}%)`;
|
|
5201
|
+
} else if (slideDirection === "right") {
|
|
5202
|
+
baseStyle.transform = `translateX(${isOpen ? 0 : 100}%)`;
|
|
5203
|
+
} else if (slideDirection === "up") {
|
|
5204
|
+
baseStyle.transform = `translateY(${isOpen ? 0 : -100}%)`;
|
|
5205
|
+
} else if (slideDirection === "down") {
|
|
5206
|
+
baseStyle.transform = `translateY(${isOpen ? 0 : 100}%)`;
|
|
5207
|
+
}
|
|
5208
|
+
break;
|
|
5209
|
+
case "fade":
|
|
5210
|
+
baseStyle.opacity = isOpen ? 1 : 0;
|
|
5211
|
+
break;
|
|
5212
|
+
case "scale":
|
|
5213
|
+
baseStyle.transform = `scale(${isOpen ? 1 : 0})`;
|
|
5214
|
+
break;
|
|
5215
|
+
case "rotate":
|
|
5216
|
+
baseStyle.transform = `rotate(${isOpen ? 0 : 180}deg)`;
|
|
5217
|
+
break;
|
|
5218
|
+
}
|
|
5219
|
+
return baseStyle;
|
|
5220
|
+
};
|
|
5221
|
+
const style = getNavigationStyle();
|
|
5222
|
+
return {
|
|
5223
|
+
ref,
|
|
5224
|
+
isVisible,
|
|
5225
|
+
isAnimating,
|
|
5226
|
+
style,
|
|
5227
|
+
progress,
|
|
5228
|
+
start,
|
|
5229
|
+
stop,
|
|
5230
|
+
reset,
|
|
5231
|
+
pause,
|
|
5232
|
+
resume,
|
|
5233
|
+
isOpen,
|
|
5234
|
+
activeIndex,
|
|
5235
|
+
itemStyles,
|
|
5236
|
+
openMenu,
|
|
5237
|
+
closeMenu,
|
|
5238
|
+
toggleMenu,
|
|
5239
|
+
setActiveItem,
|
|
5240
|
+
goToNext,
|
|
5241
|
+
goToPrevious
|
|
5242
|
+
};
|
|
5243
|
+
}
|
|
5244
|
+
function useSkeleton(options = {}) {
|
|
5245
|
+
const {
|
|
5246
|
+
delay = 0,
|
|
5247
|
+
duration = 1500,
|
|
5248
|
+
easing: easing2 = "ease-in-out",
|
|
5249
|
+
autoStart = true,
|
|
5250
|
+
backgroundColor = "#f0f0f0",
|
|
5251
|
+
highlightColor = "#e0e0e0",
|
|
5252
|
+
motionSpeed = 1500,
|
|
5253
|
+
height = 20,
|
|
5254
|
+
width = "100%",
|
|
5255
|
+
borderRadius = 4,
|
|
5256
|
+
wave = true,
|
|
5257
|
+
pulse: pulse2 = false,
|
|
5258
|
+
onComplete,
|
|
5259
|
+
onStart,
|
|
5260
|
+
onStop,
|
|
5261
|
+
onReset
|
|
5262
|
+
} = options;
|
|
5263
|
+
const ref = useRef(null);
|
|
5264
|
+
const [isVisible, setIsVisible] = useState(autoStart);
|
|
5265
|
+
const [isAnimating, setIsAnimating] = useState(autoStart);
|
|
5266
|
+
const [progress, setProgress] = useState(0);
|
|
5267
|
+
const motionRef = useRef(null);
|
|
5268
|
+
const startTimeRef = useRef(null);
|
|
5269
|
+
const start = useCallback(() => {
|
|
5270
|
+
if (isAnimating) return;
|
|
5271
|
+
setIsVisible(true);
|
|
5272
|
+
setIsAnimating(true);
|
|
5273
|
+
setProgress(0);
|
|
5274
|
+
onStart?.();
|
|
5275
|
+
setTimeout(() => {
|
|
5276
|
+
startTimeRef.current = Date.now();
|
|
5277
|
+
const animate = () => {
|
|
5278
|
+
if (!startTimeRef.current) return;
|
|
5279
|
+
const elapsed = Date.now() - startTimeRef.current;
|
|
5280
|
+
const newProgress = Math.min(elapsed / duration, 1);
|
|
5281
|
+
setProgress(newProgress);
|
|
5282
|
+
if (newProgress < 1) {
|
|
5283
|
+
motionRef.current = requestAnimationFrame(animate);
|
|
5284
|
+
} else {
|
|
5285
|
+
setIsAnimating(false);
|
|
5286
|
+
onComplete?.();
|
|
5287
|
+
}
|
|
5288
|
+
};
|
|
5289
|
+
motionRef.current = requestAnimationFrame(animate);
|
|
5290
|
+
}, delay);
|
|
5291
|
+
}, [delay, duration, isAnimating, onStart, onComplete]);
|
|
5292
|
+
const stop = useCallback(() => {
|
|
5293
|
+
if (motionRef.current) {
|
|
5294
|
+
cancelAnimationFrame(motionRef.current);
|
|
5295
|
+
motionRef.current = null;
|
|
5296
|
+
}
|
|
5297
|
+
startTimeRef.current = null;
|
|
5298
|
+
setIsAnimating(false);
|
|
5299
|
+
onStop?.();
|
|
5300
|
+
}, [onStop]);
|
|
5301
|
+
const reset = useCallback(() => {
|
|
5302
|
+
stop();
|
|
5303
|
+
setIsVisible(false);
|
|
5304
|
+
setProgress(0);
|
|
5305
|
+
onReset?.();
|
|
5306
|
+
}, [stop, onReset]);
|
|
5307
|
+
const pause = useCallback(() => {
|
|
5308
|
+
if (motionRef.current) {
|
|
5309
|
+
cancelAnimationFrame(motionRef.current);
|
|
5310
|
+
motionRef.current = null;
|
|
5311
|
+
}
|
|
5312
|
+
setIsAnimating(false);
|
|
5313
|
+
}, []);
|
|
5314
|
+
const resume = useCallback(() => {
|
|
5315
|
+
if (!isAnimating && isVisible) {
|
|
5316
|
+
start();
|
|
5317
|
+
}
|
|
5318
|
+
}, [isAnimating, isVisible, start]);
|
|
5319
|
+
useEffect(() => {
|
|
5320
|
+
if (autoStart) {
|
|
5321
|
+
start();
|
|
5322
|
+
}
|
|
5323
|
+
}, [autoStart, start]);
|
|
5324
|
+
useEffect(() => {
|
|
5325
|
+
return () => {
|
|
5326
|
+
if (motionRef.current) {
|
|
5327
|
+
cancelAnimationFrame(motionRef.current);
|
|
5328
|
+
}
|
|
5329
|
+
};
|
|
5330
|
+
}, []);
|
|
5331
|
+
const getSkeletonStyle = () => {
|
|
5332
|
+
const baseStyle = {
|
|
5333
|
+
width: typeof width === "number" ? `${width}px` : width,
|
|
5334
|
+
height: `${height}px`,
|
|
5335
|
+
backgroundColor,
|
|
5336
|
+
borderRadius: `${borderRadius}px`,
|
|
5337
|
+
position: "relative",
|
|
5338
|
+
overflow: "hidden",
|
|
5339
|
+
opacity: isVisible ? 1 : 0,
|
|
5340
|
+
transition: `opacity ${duration}ms ${easing2}`
|
|
5341
|
+
};
|
|
5342
|
+
if (wave && isAnimating) {
|
|
5343
|
+
baseStyle.background = `linear-gradient(90deg, ${backgroundColor} 25%, ${highlightColor} 50%, ${backgroundColor} 75%)`;
|
|
5344
|
+
baseStyle.backgroundSize = "200% 100%";
|
|
5345
|
+
baseStyle.animation = `skeleton-wave ${motionSpeed}ms infinite linear`;
|
|
5346
|
+
} else if (pulse2 && isAnimating) {
|
|
5347
|
+
baseStyle.animation = `skeleton-pulse ${motionSpeed}ms infinite ease-in-out`;
|
|
5348
|
+
}
|
|
5349
|
+
return baseStyle;
|
|
5350
|
+
};
|
|
5351
|
+
const style = getSkeletonStyle();
|
|
5352
|
+
useEffect(() => {
|
|
5353
|
+
const styleSheet = document.styleSheets[0] || document.createElement("style").sheet;
|
|
5354
|
+
if (styleSheet && !document.getElementById("skeleton-animations")) {
|
|
5355
|
+
const styleElement = document.createElement("style");
|
|
5356
|
+
styleElement.id = "skeleton-animations";
|
|
5357
|
+
styleElement.textContent = `
|
|
5358
|
+
@keyframes skeleton-wave {
|
|
5359
|
+
0% { background-position: 200% 0; }
|
|
5360
|
+
100% { background-position: -200% 0; }
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
@keyframes skeleton-pulse {
|
|
5364
|
+
0%, 100% { opacity: 0.6; }
|
|
5365
|
+
50% { opacity: 1; }
|
|
5366
|
+
}
|
|
5367
|
+
`;
|
|
5368
|
+
document.head.appendChild(styleElement);
|
|
5369
|
+
}
|
|
5370
|
+
}, []);
|
|
5371
|
+
return {
|
|
5372
|
+
ref,
|
|
5373
|
+
isVisible,
|
|
5374
|
+
isAnimating,
|
|
5375
|
+
style,
|
|
5376
|
+
progress,
|
|
5377
|
+
start,
|
|
5378
|
+
stop,
|
|
5379
|
+
reset,
|
|
5380
|
+
pause,
|
|
5381
|
+
resume
|
|
5382
|
+
};
|
|
5383
|
+
}
|
|
5384
|
+
function useTypewriter(options) {
|
|
5385
|
+
const { text, speed = 50, delay = 0, enabled = true, onComplete } = options;
|
|
5386
|
+
const [index, setIndex] = useState(0);
|
|
5387
|
+
const [started, setStarted] = useState(false);
|
|
5388
|
+
const timerRef = useRef(null);
|
|
5389
|
+
const restart = useCallback(() => {
|
|
5390
|
+
setIndex(0);
|
|
5391
|
+
setStarted(false);
|
|
5392
|
+
}, []);
|
|
5393
|
+
useEffect(() => {
|
|
5394
|
+
if (!enabled) return;
|
|
5395
|
+
const id = setTimeout(() => setStarted(true), delay);
|
|
5396
|
+
return () => clearTimeout(id);
|
|
5397
|
+
}, [enabled, delay]);
|
|
5398
|
+
useEffect(() => {
|
|
5399
|
+
if (!started || !enabled) return;
|
|
5400
|
+
if (index >= text.length) {
|
|
5401
|
+
onComplete?.();
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
timerRef.current = setTimeout(() => {
|
|
5405
|
+
setIndex((prev) => prev + 1);
|
|
5406
|
+
}, speed);
|
|
5407
|
+
return () => {
|
|
5408
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
5409
|
+
};
|
|
5410
|
+
}, [started, enabled, index, text.length, speed, onComplete]);
|
|
5411
|
+
useEffect(() => {
|
|
5412
|
+
setIndex(0);
|
|
5413
|
+
setStarted(false);
|
|
5414
|
+
}, [text]);
|
|
5415
|
+
return {
|
|
5416
|
+
displayText: text.slice(0, index),
|
|
5417
|
+
isTyping: started && index < text.length,
|
|
5418
|
+
progress: text.length > 0 ? index / text.length : 0,
|
|
5419
|
+
restart
|
|
5420
|
+
};
|
|
5421
|
+
}
|
|
5422
|
+
function useCustomCursor(options = {}) {
|
|
5423
|
+
const {
|
|
5424
|
+
enabled = true,
|
|
5425
|
+
size = 32,
|
|
5426
|
+
smoothing = 0.15,
|
|
5427
|
+
hoverScale = 1.5,
|
|
5428
|
+
detectLabels = true
|
|
5429
|
+
} = options;
|
|
5430
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
5431
|
+
const [label, setLabel] = useState(null);
|
|
5432
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
5433
|
+
const targetRef = useRef({ x: 0, y: 0 });
|
|
5434
|
+
const currentRef = useRef({ x: 0, y: 0 });
|
|
5435
|
+
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
5436
|
+
const rafRef = useRef(null);
|
|
5437
|
+
const animate = useCallback(() => {
|
|
5438
|
+
const dx = targetRef.current.x - currentRef.current.x;
|
|
5439
|
+
const dy = targetRef.current.y - currentRef.current.y;
|
|
5440
|
+
currentRef.current.x += dx * smoothing;
|
|
5441
|
+
currentRef.current.y += dy * smoothing;
|
|
5442
|
+
setPos({ x: currentRef.current.x, y: currentRef.current.y });
|
|
5443
|
+
if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) {
|
|
5444
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
5445
|
+
}
|
|
5446
|
+
}, [smoothing]);
|
|
5447
|
+
useEffect(() => {
|
|
5448
|
+
if (!enabled || typeof window === "undefined") return;
|
|
5449
|
+
const handleMouseMove = (e) => {
|
|
5450
|
+
targetRef.current = { x: e.clientX, y: e.clientY };
|
|
5451
|
+
setIsVisible(true);
|
|
5452
|
+
if (!rafRef.current) {
|
|
5453
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
5454
|
+
}
|
|
5455
|
+
if (detectLabels) {
|
|
5456
|
+
const target = e.target;
|
|
5457
|
+
const cursorEl = target.closest("[data-cursor]");
|
|
5458
|
+
if (cursorEl) {
|
|
5459
|
+
setLabel(cursorEl.dataset.cursor || null);
|
|
5460
|
+
setIsHovering(true);
|
|
5461
|
+
} else {
|
|
5462
|
+
setLabel(null);
|
|
5463
|
+
setIsHovering(false);
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
};
|
|
5467
|
+
const handleMouseLeave = () => {
|
|
5468
|
+
setIsVisible(false);
|
|
5469
|
+
setLabel(null);
|
|
5470
|
+
setIsHovering(false);
|
|
5471
|
+
};
|
|
5472
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
5473
|
+
document.addEventListener("mouseleave", handleMouseLeave);
|
|
5474
|
+
return () => {
|
|
5475
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
5476
|
+
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
5477
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
5478
|
+
};
|
|
5479
|
+
}, [enabled, detectLabels, animate]);
|
|
5480
|
+
const scale = isHovering ? hoverScale : 1;
|
|
5481
|
+
const style = useMemo(() => ({
|
|
5482
|
+
"--cursor-x": `${pos.x}px`,
|
|
5483
|
+
"--cursor-y": `${pos.y}px`,
|
|
5484
|
+
"--cursor-size": `${size}px`,
|
|
5485
|
+
"--cursor-scale": `${scale}`,
|
|
5486
|
+
position: "fixed",
|
|
5487
|
+
left: pos.x - size * scale / 2,
|
|
5488
|
+
top: pos.y - size * scale / 2,
|
|
5489
|
+
width: size * scale,
|
|
5490
|
+
height: size * scale,
|
|
5491
|
+
pointerEvents: "none",
|
|
5492
|
+
zIndex: 9999,
|
|
5493
|
+
transition: "width 0.2s, height 0.2s, left 0.05s, top 0.05s"
|
|
5494
|
+
}), [pos.x, pos.y, size, scale]);
|
|
5495
|
+
return { x: pos.x, y: pos.y, label, isHovering, style, isVisible };
|
|
5496
|
+
}
|
|
5497
|
+
function useMagneticCursor(options = {}) {
|
|
5498
|
+
const { strength = 0.3, radius = 100, enabled = true } = options;
|
|
5499
|
+
const ref = useRef(null);
|
|
5500
|
+
const transformRef = useRef({ x: 0, y: 0 });
|
|
5501
|
+
const styleRef = useRef({
|
|
5502
|
+
transition: "transform 0.3s cubic-bezier(0.22, 1, 0.36, 1)",
|
|
5503
|
+
transform: "translate(0px, 0px)"
|
|
5504
|
+
});
|
|
5505
|
+
const onMouseMove = useCallback((e) => {
|
|
5506
|
+
if (!enabled || !ref.current) return;
|
|
5507
|
+
const rect = ref.current.getBoundingClientRect();
|
|
5508
|
+
const centerX = rect.left + rect.width / 2;
|
|
5509
|
+
const centerY = rect.top + rect.height / 2;
|
|
5510
|
+
const dx = e.clientX - centerX;
|
|
5511
|
+
const dy = e.clientY - centerY;
|
|
5512
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
5513
|
+
if (dist < radius) {
|
|
5514
|
+
const pull = (1 - dist / radius) * strength;
|
|
5515
|
+
transformRef.current = { x: dx * pull, y: dy * pull };
|
|
5516
|
+
} else {
|
|
5517
|
+
transformRef.current = { x: 0, y: 0 };
|
|
5518
|
+
}
|
|
5519
|
+
ref.current.style.transform = `translate(${transformRef.current.x}px, ${transformRef.current.y}px)`;
|
|
5520
|
+
}, [enabled, strength, radius]);
|
|
5521
|
+
const onMouseLeave = useCallback(() => {
|
|
5522
|
+
if (!ref.current) return;
|
|
5523
|
+
transformRef.current = { x: 0, y: 0 };
|
|
5524
|
+
ref.current.style.transform = "translate(0px, 0px)";
|
|
5525
|
+
}, []);
|
|
5526
|
+
const handlers = useMemo(() => ({ onMouseMove, onMouseLeave }), [onMouseMove, onMouseLeave]);
|
|
5527
|
+
return { ref, handlers, style: styleRef.current };
|
|
5528
|
+
}
|
|
5529
|
+
function useSmoothScroll(options = {}) {
|
|
5530
|
+
const {
|
|
5531
|
+
enabled = true,
|
|
5532
|
+
lerp = 0.1,
|
|
5533
|
+
wheelMultiplier = 1,
|
|
5534
|
+
touchMultiplier = 2,
|
|
5535
|
+
direction = "vertical",
|
|
5536
|
+
onScroll: onScroll2
|
|
5537
|
+
} = options;
|
|
5538
|
+
const [scroll, setScroll] = useState(0);
|
|
5539
|
+
const [progress, setProgress] = useState(0);
|
|
5540
|
+
const targetRef = useRef(0);
|
|
5541
|
+
const currentRef = useRef(0);
|
|
5542
|
+
const rafRef = useRef(null);
|
|
5543
|
+
const isRunningRef = useRef(false);
|
|
5544
|
+
const touchStartRef = useRef(0);
|
|
5545
|
+
const getMaxScroll = useCallback(() => {
|
|
5546
|
+
if (typeof document === "undefined") return 0;
|
|
5547
|
+
return direction === "vertical" ? document.documentElement.scrollHeight - window.innerHeight : document.documentElement.scrollWidth - window.innerWidth;
|
|
5548
|
+
}, [direction]);
|
|
5549
|
+
const clamp = useCallback((val) => {
|
|
5550
|
+
return Math.max(0, Math.min(val, getMaxScroll()));
|
|
5551
|
+
}, [getMaxScroll]);
|
|
5552
|
+
const animate = useCallback(() => {
|
|
5553
|
+
const dx = targetRef.current - currentRef.current;
|
|
5554
|
+
if (Math.abs(dx) < 0.5) {
|
|
5555
|
+
currentRef.current = targetRef.current;
|
|
5556
|
+
setScroll(currentRef.current);
|
|
5557
|
+
isRunningRef.current = false;
|
|
5558
|
+
return;
|
|
5559
|
+
}
|
|
5560
|
+
currentRef.current += dx * lerp;
|
|
5561
|
+
setScroll(currentRef.current);
|
|
5562
|
+
const max = getMaxScroll();
|
|
5563
|
+
setProgress(max > 0 ? currentRef.current / max : 0);
|
|
5564
|
+
onScroll2?.(currentRef.current);
|
|
5565
|
+
if (direction === "vertical") {
|
|
5566
|
+
window.scrollTo(0, currentRef.current);
|
|
5567
|
+
} else {
|
|
5568
|
+
window.scrollTo(currentRef.current, 0);
|
|
5569
|
+
}
|
|
5570
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
5571
|
+
}, [lerp, direction, getMaxScroll, onScroll2]);
|
|
5572
|
+
const startAnimation = useCallback(() => {
|
|
5573
|
+
if (isRunningRef.current) return;
|
|
5574
|
+
isRunningRef.current = true;
|
|
5575
|
+
rafRef.current = requestAnimationFrame(animate);
|
|
5576
|
+
}, [animate]);
|
|
5577
|
+
useEffect(() => {
|
|
5578
|
+
if (!enabled || typeof window === "undefined") return;
|
|
5579
|
+
document.documentElement.style.scrollBehavior = "auto";
|
|
5580
|
+
currentRef.current = direction === "vertical" ? window.scrollY : window.scrollX;
|
|
5581
|
+
targetRef.current = currentRef.current;
|
|
5582
|
+
const handleWheel = (e) => {
|
|
5583
|
+
e.preventDefault();
|
|
5584
|
+
const delta = direction === "vertical" ? e.deltaY : e.deltaX;
|
|
5585
|
+
targetRef.current = clamp(targetRef.current + delta * wheelMultiplier);
|
|
5586
|
+
startAnimation();
|
|
5587
|
+
};
|
|
5588
|
+
const handleTouchStart = (e) => {
|
|
5589
|
+
touchStartRef.current = direction === "vertical" ? e.touches[0].clientY : e.touches[0].clientX;
|
|
5590
|
+
};
|
|
5591
|
+
const handleTouchMove = (e) => {
|
|
5592
|
+
const current = direction === "vertical" ? e.touches[0].clientY : e.touches[0].clientX;
|
|
5593
|
+
const delta = (touchStartRef.current - current) * touchMultiplier;
|
|
5594
|
+
touchStartRef.current = current;
|
|
5595
|
+
targetRef.current = clamp(targetRef.current + delta);
|
|
5596
|
+
startAnimation();
|
|
5597
|
+
};
|
|
5598
|
+
window.addEventListener("wheel", handleWheel, { passive: false });
|
|
5599
|
+
window.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
5600
|
+
window.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
5601
|
+
return () => {
|
|
5602
|
+
window.removeEventListener("wheel", handleWheel);
|
|
5603
|
+
window.removeEventListener("touchstart", handleTouchStart);
|
|
5604
|
+
window.removeEventListener("touchmove", handleTouchMove);
|
|
5605
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
5606
|
+
document.documentElement.style.scrollBehavior = "";
|
|
5607
|
+
};
|
|
5608
|
+
}, [enabled, direction, wheelMultiplier, touchMultiplier, clamp, startAnimation]);
|
|
5609
|
+
const scrollTo = useCallback((target, opts) => {
|
|
5610
|
+
const offset = opts?.offset ?? 0;
|
|
5611
|
+
if (typeof target === "number") {
|
|
5612
|
+
targetRef.current = clamp(target + offset);
|
|
5613
|
+
} else {
|
|
5614
|
+
const rect = target.getBoundingClientRect();
|
|
5615
|
+
const pos = direction === "vertical" ? rect.top + currentRef.current + offset : rect.left + currentRef.current + offset;
|
|
5616
|
+
targetRef.current = clamp(pos);
|
|
5617
|
+
}
|
|
5618
|
+
startAnimation();
|
|
5619
|
+
}, [clamp, direction, startAnimation]);
|
|
5620
|
+
const stop = useCallback(() => {
|
|
5621
|
+
if (rafRef.current) {
|
|
5622
|
+
cancelAnimationFrame(rafRef.current);
|
|
5623
|
+
rafRef.current = null;
|
|
5624
|
+
}
|
|
5625
|
+
isRunningRef.current = false;
|
|
5626
|
+
targetRef.current = currentRef.current;
|
|
5627
|
+
}, []);
|
|
5628
|
+
return {
|
|
5629
|
+
scroll,
|
|
5630
|
+
targetScroll: targetRef.current,
|
|
5631
|
+
progress,
|
|
5632
|
+
scrollTo,
|
|
5633
|
+
stop
|
|
5634
|
+
};
|
|
5635
|
+
}
|
|
5636
|
+
function useElementProgress(options = {}) {
|
|
5637
|
+
const { start = 0, end = 1, clamp = true } = options;
|
|
5638
|
+
const ref = useRef(null);
|
|
5639
|
+
const [progress, setProgress] = useState(0);
|
|
5640
|
+
const [isInView, setIsInView] = useState(false);
|
|
5641
|
+
const progressRef = useRef(0);
|
|
5642
|
+
const isInViewRef = useRef(false);
|
|
5643
|
+
useEffect(() => {
|
|
5644
|
+
const el = ref.current;
|
|
5645
|
+
if (!el || typeof window === "undefined") return;
|
|
5646
|
+
const calculate = () => {
|
|
5647
|
+
const rect = el.getBoundingClientRect();
|
|
5648
|
+
const vh = window.innerHeight;
|
|
5649
|
+
const elementTop = rect.top;
|
|
5650
|
+
const elementBottom = rect.bottom;
|
|
5651
|
+
const trackStart = vh * (1 - start);
|
|
5652
|
+
const trackEnd = vh * end * -1 + vh;
|
|
5653
|
+
const range = trackStart - trackEnd;
|
|
5654
|
+
const raw = range > 0 ? (trackStart - elementTop) / range : 0;
|
|
5655
|
+
const clamped = clamp ? Math.max(0, Math.min(1, raw)) : raw;
|
|
5656
|
+
if (Math.abs(clamped - progressRef.current) > 5e-3) {
|
|
5657
|
+
progressRef.current = clamped;
|
|
5658
|
+
setProgress(clamped);
|
|
5659
|
+
}
|
|
5660
|
+
const nowInView = elementBottom > 0 && elementTop < vh;
|
|
5661
|
+
if (nowInView !== isInViewRef.current) {
|
|
5662
|
+
isInViewRef.current = nowInView;
|
|
5663
|
+
setIsInView(nowInView);
|
|
5664
|
+
}
|
|
5665
|
+
};
|
|
5666
|
+
calculate();
|
|
5667
|
+
return subscribeScroll(calculate);
|
|
5668
|
+
}, [start, end, clamp]);
|
|
5669
|
+
return { ref, progress, isInView };
|
|
5670
|
+
}
|
|
5671
|
+
function Motion({
|
|
5672
|
+
as: Component = "div",
|
|
5673
|
+
type,
|
|
5674
|
+
effects,
|
|
5675
|
+
scroll,
|
|
5676
|
+
delay,
|
|
5677
|
+
duration,
|
|
5678
|
+
children,
|
|
5679
|
+
className,
|
|
5680
|
+
style: userStyle,
|
|
5681
|
+
...rest
|
|
5682
|
+
}) {
|
|
5683
|
+
const scrollOptions = useMemo(() => {
|
|
5684
|
+
if (!scroll) return null;
|
|
5685
|
+
const base = typeof scroll === "object" ? scroll : {};
|
|
5686
|
+
return {
|
|
5687
|
+
...base,
|
|
5688
|
+
...delay != null && { delay },
|
|
5689
|
+
...duration != null && { duration },
|
|
5690
|
+
...type != null && { motionType: type }
|
|
5691
|
+
};
|
|
5692
|
+
}, [scroll, delay, duration, type]);
|
|
5693
|
+
const scrollMotion = useScrollReveal(scrollOptions ?? { delay: 0 });
|
|
5694
|
+
const unifiedMotion = useUnifiedMotion({
|
|
5695
|
+
type: type ?? "fadeIn",
|
|
5696
|
+
effects,
|
|
5697
|
+
delay,
|
|
5698
|
+
duration,
|
|
5699
|
+
autoStart: true
|
|
5700
|
+
});
|
|
5701
|
+
const isScroll = scroll != null && scroll !== false;
|
|
5702
|
+
const motion = isScroll ? scrollMotion : unifiedMotion;
|
|
5703
|
+
const mergedStyle = useMemo(() => {
|
|
5704
|
+
if (!userStyle) return motion.style;
|
|
5705
|
+
return { ...motion.style, ...userStyle };
|
|
5706
|
+
}, [motion.style, userStyle]);
|
|
5707
|
+
return /* @__PURE__ */ jsx(
|
|
5708
|
+
Component,
|
|
5709
|
+
{
|
|
5710
|
+
ref: motion.ref,
|
|
5711
|
+
className,
|
|
5712
|
+
style: mergedStyle,
|
|
5713
|
+
...rest,
|
|
5714
|
+
children
|
|
5715
|
+
}
|
|
5716
|
+
);
|
|
5717
|
+
}
|
|
5718
|
+
function getInitialTransform(motionType, slideDistance, scaleFrom) {
|
|
5719
|
+
switch (motionType) {
|
|
5720
|
+
case "slideUp":
|
|
5721
|
+
return `translateY(${slideDistance}px)`;
|
|
5722
|
+
case "slideLeft":
|
|
5723
|
+
return `translateX(-${slideDistance}px)`;
|
|
5724
|
+
case "slideRight":
|
|
5725
|
+
return `translateX(${slideDistance}px)`;
|
|
5726
|
+
case "scaleIn":
|
|
5727
|
+
return `scale(${scaleFrom})`;
|
|
5728
|
+
case "bounceIn":
|
|
5729
|
+
return "scale(0.75)";
|
|
5730
|
+
case "fadeIn":
|
|
5731
|
+
default:
|
|
5732
|
+
return "none";
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
function useStagger(options) {
|
|
5736
|
+
const profile = useMotionProfile();
|
|
5737
|
+
const {
|
|
5738
|
+
count,
|
|
5739
|
+
staggerDelay = profile.stagger.perItem,
|
|
5740
|
+
baseDelay = profile.stagger.baseDelay,
|
|
5741
|
+
duration = profile.base.duration,
|
|
5742
|
+
motionType = "fadeIn",
|
|
5743
|
+
threshold = profile.base.threshold,
|
|
5744
|
+
easing: easing2 = profile.base.easing
|
|
5745
|
+
} = options;
|
|
5746
|
+
const containerRef = useRef(null);
|
|
5747
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
5748
|
+
useEffect(() => {
|
|
5749
|
+
if (!containerRef.current) return;
|
|
5750
|
+
return observeElement(
|
|
5751
|
+
containerRef.current,
|
|
5752
|
+
(entry) => {
|
|
5753
|
+
if (entry.isIntersecting) setIsVisible(true);
|
|
5754
|
+
},
|
|
5755
|
+
{ threshold },
|
|
5756
|
+
true
|
|
5757
|
+
// once — 첫 intersection 후 자동 정리
|
|
5758
|
+
);
|
|
5759
|
+
}, [threshold]);
|
|
5760
|
+
const slideDistance = profile.entrance.slide.distance;
|
|
5761
|
+
const scaleFrom = profile.entrance.scale.from;
|
|
5762
|
+
const initialTransform = useMemo(() => getInitialTransform(motionType, slideDistance, scaleFrom), [motionType, slideDistance, scaleFrom]);
|
|
5763
|
+
const styles = useMemo(() => {
|
|
5764
|
+
return Array.from({ length: count }, (_, i) => {
|
|
5765
|
+
const itemDelay = baseDelay + i * staggerDelay;
|
|
5766
|
+
if (!isVisible) {
|
|
5767
|
+
return {
|
|
5768
|
+
opacity: 0,
|
|
5769
|
+
transform: initialTransform,
|
|
5770
|
+
transition: `opacity ${duration}ms ${easing2} ${itemDelay}ms, transform ${duration}ms ${easing2} ${itemDelay}ms`
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
return {
|
|
5774
|
+
opacity: 1,
|
|
5775
|
+
transform: "none",
|
|
5776
|
+
transition: `opacity ${duration}ms ${easing2} ${itemDelay}ms, transform ${duration}ms ${easing2} ${itemDelay}ms`
|
|
5777
|
+
};
|
|
5778
|
+
});
|
|
5779
|
+
}, [count, isVisible, staggerDelay, baseDelay, duration, motionType, easing2, initialTransform]);
|
|
5780
|
+
return {
|
|
5781
|
+
containerRef,
|
|
5782
|
+
styles,
|
|
5783
|
+
isVisible
|
|
5784
|
+
};
|
|
5785
|
+
}
|
|
4191
5786
|
|
|
4192
|
-
export { MOTION_PRESETS, MotionEngine,
|
|
5787
|
+
export { MOTION_PRESETS, Motion, MotionEngine, MotionProfileProvider, PAGE_MOTIONS, TransitionEffects, applyEasing, calculateSpring, easeIn, easeInOut, easeInOutQuad, easeInQuad, easeOut, easeOutQuad, easingPresets, getAvailableEasings, getEasing, getMotionPreset, getPagePreset, getPresetEasing, hua, isEasingFunction, isValidEasing, linear, mergeProfileOverrides, mergeWithPreset, motionEngine, neutral, observeElement, resolveProfile, safeApplyEasing, transitionEffects, useBounceIn, useButtonEffect, useCardList, useClickToggle, useCustomCursor, useElementProgress, useFadeIn, useFocusToggle, useGesture, useGestureMotion, useGradient, useHoverMotion, useInView, useLoadingSpinner, useMagneticCursor, useMotionProfile, useMotionState, useMouse, useNavigation, usePageMotions, usePulse, useReducedMotion, useRepeat, useScaleIn, useScrollProgress, useScrollReveal, useScrollToggle, useSimplePageMotion, useSkeleton, useSlideDown, useSlideLeft, useSlideRight, useSlideUp, useSmartMotion, useSmoothScroll, useSpringMotion, useStagger, useToggleMotion, useTypewriter, useUnifiedMotion, useVisibilityToggle, useWindowSize };
|
|
4193
5788
|
//# sourceMappingURL=index.mjs.map
|
|
4194
5789
|
//# sourceMappingURL=index.mjs.map
|