r3f-motion 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +569 -5
- package/dist/es/components/Carousel/index.mjs +147 -0
- package/dist/es/index.mjs +1 -0
- package/dist/es/render/gestures/use-drag.mjs +389 -0
- package/dist/es/render/motion.mjs +17 -1
- package/dist/es/render/use-render.mjs +7 -0
- package/dist/index.d.ts +58 -4
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -4,9 +4,29 @@
|
|
|
4
4
|
var tslib = require('tslib');
|
|
5
5
|
var react = require('react');
|
|
6
6
|
var motion$1 = require('motion');
|
|
7
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
8
7
|
var fiber = require('@react-three/fiber');
|
|
9
|
-
var
|
|
8
|
+
var THREE = require('three');
|
|
9
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
10
|
+
var react$1 = require('motion/react');
|
|
11
|
+
|
|
12
|
+
function _interopNamespaceDefault(e) {
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
|
|
10
30
|
|
|
11
31
|
const PresenceContext = react.createContext(null);
|
|
12
32
|
/**
|
|
@@ -32,6 +52,7 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
|
|
|
32
52
|
* Create a callback ref that captures the Three.js instance
|
|
33
53
|
*/
|
|
34
54
|
const callbackRef = react.useCallback((instance) => {
|
|
55
|
+
var _a;
|
|
35
56
|
if (!instance)
|
|
36
57
|
return;
|
|
37
58
|
// Apply initial values immediately to prevent FOUC - but only once
|
|
@@ -70,6 +91,12 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
|
|
|
70
91
|
colorProp.set(initialValues[key]);
|
|
71
92
|
}
|
|
72
93
|
}
|
|
94
|
+
else if (instance.uniforms &&
|
|
95
|
+
instance.uniforms[key] &&
|
|
96
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
97
|
+
// Set ShaderMaterial uniform initial value
|
|
98
|
+
instance.uniforms[key].value = initialValues[key];
|
|
99
|
+
}
|
|
73
100
|
else if (key in instance && typeof instance[key] === "number") {
|
|
74
101
|
instance[key] = initialValues[key];
|
|
75
102
|
}
|
|
@@ -242,6 +269,389 @@ function useTap(isStatic, props, options) {
|
|
|
242
269
|
};
|
|
243
270
|
}
|
|
244
271
|
|
|
272
|
+
// World-space units per CSS pixel at the given depth from the camera.
|
|
273
|
+
const getWorldPerPixel = (camera, depth, viewportHeight) => {
|
|
274
|
+
var _a;
|
|
275
|
+
const ortho = camera;
|
|
276
|
+
if (ortho.isOrthographicCamera) {
|
|
277
|
+
return ((ortho.top - ortho.bottom) / (viewportHeight * (ortho.zoom || 1)));
|
|
278
|
+
}
|
|
279
|
+
const persp = camera;
|
|
280
|
+
const fov = ((_a = persp.fov) !== null && _a !== void 0 ? _a : 60) * (Math.PI / 180);
|
|
281
|
+
return (2 * Math.tan(fov / 2) * depth) / viewportHeight;
|
|
282
|
+
};
|
|
283
|
+
const clampWithElastic = (value, min, max, elastic) => {
|
|
284
|
+
if (min !== undefined && value < min)
|
|
285
|
+
return min + (value - min) * elastic;
|
|
286
|
+
if (max !== undefined && value > max)
|
|
287
|
+
return max + (value - max) * elastic;
|
|
288
|
+
return value;
|
|
289
|
+
};
|
|
290
|
+
function useDrag(isStatic, props, options) {
|
|
291
|
+
const { camera, gl } = fiber.useThree();
|
|
292
|
+
const { drag, whileDrag, onDragStart, onPointerDown, transition } = props;
|
|
293
|
+
const { instanceRef, captureInstanceState, buildTargetFromState, animateToTarget, resolveVariant, stopAnimation, } = options;
|
|
294
|
+
const isDraggingRef = react.useRef(false);
|
|
295
|
+
const dragStartPointerRef = react.useRef({ x: 0, y: 0 });
|
|
296
|
+
const dragStartPosRef = react.useRef(new THREE__namespace.Vector3());
|
|
297
|
+
const cameraRightRef = react.useRef(new THREE__namespace.Vector3());
|
|
298
|
+
const cameraUpRef = react.useRef(new THREE__namespace.Vector3());
|
|
299
|
+
const worldPerPixelRef = react.useRef(0);
|
|
300
|
+
const targetPosRef = react.useRef(new THREE__namespace.Vector3());
|
|
301
|
+
const lastPosRef = react.useRef(new THREE__namespace.Vector3());
|
|
302
|
+
const velocityRef = react.useRef(new THREE__namespace.Vector3());
|
|
303
|
+
const lastDeltaRef = react.useRef(new THREE__namespace.Vector3());
|
|
304
|
+
const lastTimeRef = react.useRef(0);
|
|
305
|
+
const preDragStateRef = react.useRef(null);
|
|
306
|
+
const pointerIdRef = react.useRef(null);
|
|
307
|
+
const springStateRef = react.useRef(null);
|
|
308
|
+
const lastFrameTimeRef = react.useRef(0);
|
|
309
|
+
const propsRef = react.useRef(props);
|
|
310
|
+
propsRef.current = props;
|
|
311
|
+
fiber.useFrame(() => {
|
|
312
|
+
var _a;
|
|
313
|
+
const instance = instanceRef.current;
|
|
314
|
+
if (!instance)
|
|
315
|
+
return;
|
|
316
|
+
const obj = instance;
|
|
317
|
+
// Phase 1: dragging — apply the drag target
|
|
318
|
+
if (isDraggingRef.current) {
|
|
319
|
+
const t = targetPosRef.current;
|
|
320
|
+
obj.position.set(t.x, t.y, t.z);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Phase 2: spring (momentum / snap-to-origin)
|
|
324
|
+
const s = springStateRef.current;
|
|
325
|
+
if (!s || !s.active)
|
|
326
|
+
return;
|
|
327
|
+
const now = performance.now();
|
|
328
|
+
let dt = (now - lastFrameTimeRef.current) / 1000;
|
|
329
|
+
if (dt > 0.05)
|
|
330
|
+
dt = 0.05;
|
|
331
|
+
if (dt <= 0)
|
|
332
|
+
dt = 1 / 60;
|
|
333
|
+
lastFrameTimeRef.current = now;
|
|
334
|
+
// Sub-step the integrator for stability with stiff springs
|
|
335
|
+
const subSteps = 4;
|
|
336
|
+
const subDt = dt / subSteps;
|
|
337
|
+
for (let i = 0; i < subSteps; i++) {
|
|
338
|
+
if (s.hasX) {
|
|
339
|
+
const force = -s.stiffness * (obj.position.x - s.targetX) - s.damping * s.velX;
|
|
340
|
+
s.velX += force * subDt;
|
|
341
|
+
obj.position.x += s.velX * subDt;
|
|
342
|
+
}
|
|
343
|
+
if (s.hasY) {
|
|
344
|
+
const force = -s.stiffness * (obj.position.y - s.targetY) - s.damping * s.velY;
|
|
345
|
+
s.velY += force * subDt;
|
|
346
|
+
obj.position.y += s.velY * subDt;
|
|
347
|
+
}
|
|
348
|
+
if (s.hasZ) {
|
|
349
|
+
const force = -s.stiffness * (obj.position.z - s.targetZ) - s.damping * s.velZ;
|
|
350
|
+
s.velZ += force * subDt;
|
|
351
|
+
obj.position.z += s.velZ * subDt;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Rest detection — stop when both speed and offset are tiny
|
|
355
|
+
const speedSq = s.velX * s.velX + s.velY * s.velY + s.velZ * s.velZ;
|
|
356
|
+
const offX = s.hasX ? obj.position.x - s.targetX : 0;
|
|
357
|
+
const offY = s.hasY ? obj.position.y - s.targetY : 0;
|
|
358
|
+
const offZ = s.hasZ ? obj.position.z - s.targetZ : 0;
|
|
359
|
+
const offsetSq = offX * offX + offY * offY + offZ * offZ;
|
|
360
|
+
if (speedSq < 0.0005 && offsetSq < 0.0005) {
|
|
361
|
+
if (s.hasX)
|
|
362
|
+
obj.position.x = s.targetX;
|
|
363
|
+
if (s.hasY)
|
|
364
|
+
obj.position.y = s.targetY;
|
|
365
|
+
if (s.hasZ)
|
|
366
|
+
obj.position.z = s.targetZ;
|
|
367
|
+
s.active = false;
|
|
368
|
+
(_a = s.onComplete) === null || _a === void 0 ? void 0 : _a.call(s);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
const handlePointerDown = react.useCallback((event) => {
|
|
372
|
+
const instance = instanceRef.current;
|
|
373
|
+
if (!instance)
|
|
374
|
+
return;
|
|
375
|
+
event.stopPropagation();
|
|
376
|
+
// Halt any in-flight motion-library animation (e.g. main `animate` prop
|
|
377
|
+
// tween that's still running) and any active drag spring.
|
|
378
|
+
stopAnimation();
|
|
379
|
+
if (springStateRef.current)
|
|
380
|
+
springStateRef.current.active = false;
|
|
381
|
+
if (whileDrag) {
|
|
382
|
+
preDragStateRef.current = captureInstanceState();
|
|
383
|
+
const targetValues = typeof whileDrag === "string"
|
|
384
|
+
? resolveVariant(whileDrag)
|
|
385
|
+
: whileDrag;
|
|
386
|
+
animateToTarget(targetValues, transition || { duration: 0.1 });
|
|
387
|
+
}
|
|
388
|
+
const obj = instance;
|
|
389
|
+
// Capture initial pointer + position. All subsequent moves are computed
|
|
390
|
+
// as `initialPos + pixelDelta * worldPerPixel` — never re-derived from
|
|
391
|
+
// the live (potentially-mid-animation) instance position.
|
|
392
|
+
dragStartPointerRef.current.x = event.nativeEvent.clientX;
|
|
393
|
+
dragStartPointerRef.current.y = event.nativeEvent.clientY;
|
|
394
|
+
dragStartPosRef.current.set(obj.position.x, obj.position.y, obj.position.z);
|
|
395
|
+
targetPosRef.current.copy(dragStartPosRef.current);
|
|
396
|
+
lastPosRef.current.copy(dragStartPosRef.current);
|
|
397
|
+
lastDeltaRef.current.set(0, 0, 0);
|
|
398
|
+
velocityRef.current.set(0, 0, 0);
|
|
399
|
+
lastTimeRef.current = performance.now();
|
|
400
|
+
// Compute world-units-per-pixel at the object's perpendicular depth
|
|
401
|
+
// (distance from camera along its forward axis).
|
|
402
|
+
const cameraForward = new THREE__namespace.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
|
403
|
+
const cameraToObj = new THREE__namespace.Vector3().subVectors(dragStartPosRef.current, camera.position);
|
|
404
|
+
const depth = Math.abs(cameraToObj.dot(cameraForward));
|
|
405
|
+
const rect = gl.domElement.getBoundingClientRect();
|
|
406
|
+
worldPerPixelRef.current = getWorldPerPixel(camera, depth, rect.height);
|
|
407
|
+
// Cache camera basis vectors so screen X/Y maps correctly to world
|
|
408
|
+
// X/Y/Z even if the camera is tilted.
|
|
409
|
+
cameraRightRef.current.set(1, 0, 0).applyQuaternion(camera.quaternion);
|
|
410
|
+
cameraUpRef.current.set(0, 1, 0).applyQuaternion(camera.quaternion);
|
|
411
|
+
isDraggingRef.current = true;
|
|
412
|
+
pointerIdRef.current = event.nativeEvent.pointerId;
|
|
413
|
+
try {
|
|
414
|
+
gl.domElement.setPointerCapture(event.nativeEvent.pointerId);
|
|
415
|
+
}
|
|
416
|
+
catch (_a) {
|
|
417
|
+
// not all environments support setPointerCapture
|
|
418
|
+
}
|
|
419
|
+
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(event.nativeEvent, {
|
|
420
|
+
point: { x: event.nativeEvent.clientX, y: event.nativeEvent.clientY },
|
|
421
|
+
offset: { x: 0, y: 0, z: 0 },
|
|
422
|
+
delta: { x: 0, y: 0, z: 0 },
|
|
423
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
424
|
+
});
|
|
425
|
+
onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
|
|
426
|
+
},
|
|
427
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
428
|
+
[
|
|
429
|
+
whileDrag,
|
|
430
|
+
onDragStart,
|
|
431
|
+
onPointerDown,
|
|
432
|
+
camera,
|
|
433
|
+
gl,
|
|
434
|
+
captureInstanceState,
|
|
435
|
+
resolveVariant,
|
|
436
|
+
animateToTarget,
|
|
437
|
+
stopAnimation,
|
|
438
|
+
transition,
|
|
439
|
+
instanceRef,
|
|
440
|
+
]);
|
|
441
|
+
react.useEffect(() => {
|
|
442
|
+
const handlePointerMove = (event) => {
|
|
443
|
+
var _a;
|
|
444
|
+
if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
|
|
445
|
+
return;
|
|
446
|
+
const instance = instanceRef.current;
|
|
447
|
+
if (!instance)
|
|
448
|
+
return;
|
|
449
|
+
const cur = propsRef.current;
|
|
450
|
+
const dragAxis = cur.drag;
|
|
451
|
+
const elasticVal = typeof cur.dragElastic === "number"
|
|
452
|
+
? cur.dragElastic
|
|
453
|
+
: cur.dragElastic === false
|
|
454
|
+
? 0
|
|
455
|
+
: 0.5;
|
|
456
|
+
const constraints = cur.dragConstraints;
|
|
457
|
+
const pixelDx = event.clientX - dragStartPointerRef.current.x;
|
|
458
|
+
const pixelDy = event.clientY - dragStartPointerRef.current.y;
|
|
459
|
+
const scale = worldPerPixelRef.current;
|
|
460
|
+
let newX = dragStartPosRef.current.x;
|
|
461
|
+
let newY = dragStartPosRef.current.y;
|
|
462
|
+
let newZ = dragStartPosRef.current.z;
|
|
463
|
+
if (dragAxis === "z") {
|
|
464
|
+
// Vertical screen movement → Z. Cursor down (pixelDy > 0) = closer.
|
|
465
|
+
newZ = dragStartPosRef.current.z + pixelDy * scale;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// Map screen XY to camera-relative world delta.
|
|
469
|
+
const r = cameraRightRef.current;
|
|
470
|
+
const u = cameraUpRef.current;
|
|
471
|
+
const worldDx = r.x * pixelDx * scale + u.x * -pixelDy * scale;
|
|
472
|
+
const worldDy = r.y * pixelDx * scale + u.y * -pixelDy * scale;
|
|
473
|
+
const worldDz = r.z * pixelDx * scale + u.z * -pixelDy * scale;
|
|
474
|
+
if (dragAxis === "x") {
|
|
475
|
+
newX = dragStartPosRef.current.x + worldDx;
|
|
476
|
+
}
|
|
477
|
+
else if (dragAxis === "y") {
|
|
478
|
+
newY = dragStartPosRef.current.y + worldDy;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
newX = dragStartPosRef.current.x + worldDx;
|
|
482
|
+
newY = dragStartPosRef.current.y + worldDy;
|
|
483
|
+
newZ = dragStartPosRef.current.z + worldDz;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Constraints with elastic rubber-banding
|
|
487
|
+
if (constraints) {
|
|
488
|
+
if (dragAxis !== "y" && dragAxis !== "z") {
|
|
489
|
+
newX = clampWithElastic(newX, constraints.left, constraints.right, elasticVal);
|
|
490
|
+
}
|
|
491
|
+
if (dragAxis !== "x" && dragAxis !== "z") {
|
|
492
|
+
newY = clampWithElastic(newY, constraints.bottom, constraints.top, elasticVal);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Velocity tracking (used for momentum on release)
|
|
496
|
+
const now = performance.now();
|
|
497
|
+
const dt = (now - lastTimeRef.current) / 1000;
|
|
498
|
+
if (dt > 0) {
|
|
499
|
+
const dx = newX - lastPosRef.current.x;
|
|
500
|
+
const dy = newY - lastPosRef.current.y;
|
|
501
|
+
const dz = newZ - lastPosRef.current.z;
|
|
502
|
+
velocityRef.current.set(dx / dt, dy / dt, dz / dt);
|
|
503
|
+
lastDeltaRef.current.set(dx, dy, dz);
|
|
504
|
+
}
|
|
505
|
+
lastTimeRef.current = now;
|
|
506
|
+
lastPosRef.current.set(newX, newY, newZ);
|
|
507
|
+
// Stash target — useFrame applies it next R3F frame.
|
|
508
|
+
targetPosRef.current.set(newX, newY, newZ);
|
|
509
|
+
const offset = {
|
|
510
|
+
x: newX - dragStartPosRef.current.x,
|
|
511
|
+
y: newY - dragStartPosRef.current.y,
|
|
512
|
+
z: newZ - dragStartPosRef.current.z,
|
|
513
|
+
};
|
|
514
|
+
(_a = cur.onDrag) === null || _a === void 0 ? void 0 : _a.call(cur, event, {
|
|
515
|
+
point: { x: event.clientX, y: event.clientY },
|
|
516
|
+
offset,
|
|
517
|
+
delta: {
|
|
518
|
+
x: lastDeltaRef.current.x,
|
|
519
|
+
y: lastDeltaRef.current.y,
|
|
520
|
+
z: lastDeltaRef.current.z,
|
|
521
|
+
},
|
|
522
|
+
velocity: {
|
|
523
|
+
x: velocityRef.current.x,
|
|
524
|
+
y: velocityRef.current.y,
|
|
525
|
+
z: velocityRef.current.z,
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
const handlePointerUp = (event) => {
|
|
530
|
+
var _a, _b;
|
|
531
|
+
if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
|
|
532
|
+
return;
|
|
533
|
+
isDraggingRef.current = false;
|
|
534
|
+
pointerIdRef.current = null;
|
|
535
|
+
const instance = instanceRef.current;
|
|
536
|
+
const cur = propsRef.current;
|
|
537
|
+
// Lock in the final dragged position synchronously. Once useFrame stops
|
|
538
|
+
// applying the drag target, a still-pending write from the *previous*
|
|
539
|
+
// momentum animation can land before the new animateToTarget() call
|
|
540
|
+
// reads instance.position to use as its "from" value — which causes the
|
|
541
|
+
// new animation to start from the old momentum's path instead of where
|
|
542
|
+
// the user actually released.
|
|
543
|
+
if (instance) {
|
|
544
|
+
const obj = instance;
|
|
545
|
+
const t = targetPosRef.current;
|
|
546
|
+
obj.position.set(t.x, t.y, t.z);
|
|
547
|
+
}
|
|
548
|
+
// Restore whileDrag visual state
|
|
549
|
+
if (cur.whileDrag && preDragStateRef.current) {
|
|
550
|
+
const targetValues = buildTargetFromState(preDragStateRef.current);
|
|
551
|
+
animateToTarget(targetValues, options.transition || { duration: 0.2 });
|
|
552
|
+
const dur = ((_a = options.transition) === null || _a === void 0 ? void 0 : _a.duration) || 0.2;
|
|
553
|
+
setTimeout(() => {
|
|
554
|
+
preDragStateRef.current = null;
|
|
555
|
+
}, dur * 1000);
|
|
556
|
+
}
|
|
557
|
+
if (!instance)
|
|
558
|
+
return;
|
|
559
|
+
const finalPos = targetPosRef.current;
|
|
560
|
+
const offset = {
|
|
561
|
+
x: finalPos.x - dragStartPosRef.current.x,
|
|
562
|
+
y: finalPos.y - dragStartPosRef.current.y,
|
|
563
|
+
z: finalPos.z - dragStartPosRef.current.z,
|
|
564
|
+
};
|
|
565
|
+
const vel = velocityRef.current;
|
|
566
|
+
// Read spring tuning from dragTransition (if user provided one).
|
|
567
|
+
const dt = cur.dragTransition;
|
|
568
|
+
const userStiffness = typeof (dt === null || dt === void 0 ? void 0 : dt.stiffness) === "number" ? dt.stiffness : undefined;
|
|
569
|
+
const userDamping = typeof (dt === null || dt === void 0 ? void 0 : dt.damping) === "number" ? dt.damping : undefined;
|
|
570
|
+
if (cur.dragSnapToOrigin) {
|
|
571
|
+
lastFrameTimeRef.current = performance.now();
|
|
572
|
+
springStateRef.current = {
|
|
573
|
+
active: true,
|
|
574
|
+
targetX: dragStartPosRef.current.x,
|
|
575
|
+
targetY: dragStartPosRef.current.y,
|
|
576
|
+
targetZ: dragStartPosRef.current.z,
|
|
577
|
+
velX: vel.x,
|
|
578
|
+
velY: vel.y,
|
|
579
|
+
velZ: vel.z,
|
|
580
|
+
hasX: true,
|
|
581
|
+
hasY: true,
|
|
582
|
+
hasZ: true,
|
|
583
|
+
stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 400,
|
|
584
|
+
damping: userDamping !== null && userDamping !== void 0 ? userDamping : 40,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
else if (cur.dragMomentum !== false) {
|
|
588
|
+
const speed = Math.sqrt(Math.pow(vel.x, 2) + Math.pow(vel.y, 2) + Math.pow(vel.z, 2));
|
|
589
|
+
if (speed > 0.5) {
|
|
590
|
+
const coeff = 0.25;
|
|
591
|
+
const dragAxis = cur.drag;
|
|
592
|
+
const hasX = dragAxis !== "y" && dragAxis !== "z";
|
|
593
|
+
const hasY = dragAxis !== "x" && dragAxis !== "z";
|
|
594
|
+
const hasZ = dragAxis === "z";
|
|
595
|
+
let targetX = hasX ? finalPos.x + vel.x * coeff : finalPos.x;
|
|
596
|
+
let targetY = hasY ? finalPos.y + vel.y * coeff : finalPos.y;
|
|
597
|
+
const targetZ = hasZ ? finalPos.z + vel.z * coeff : finalPos.z;
|
|
598
|
+
const constraints = cur.dragConstraints;
|
|
599
|
+
if (constraints) {
|
|
600
|
+
if (hasX) {
|
|
601
|
+
if (constraints.left !== undefined)
|
|
602
|
+
targetX = Math.max(targetX, constraints.left);
|
|
603
|
+
if (constraints.right !== undefined)
|
|
604
|
+
targetX = Math.min(targetX, constraints.right);
|
|
605
|
+
}
|
|
606
|
+
if (hasY) {
|
|
607
|
+
if (constraints.bottom !== undefined)
|
|
608
|
+
targetY = Math.max(targetY, constraints.bottom);
|
|
609
|
+
if (constraints.top !== undefined)
|
|
610
|
+
targetY = Math.min(targetY, constraints.top);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
lastFrameTimeRef.current = performance.now();
|
|
614
|
+
springStateRef.current = {
|
|
615
|
+
active: true,
|
|
616
|
+
targetX,
|
|
617
|
+
targetY,
|
|
618
|
+
targetZ,
|
|
619
|
+
velX: hasX ? vel.x : 0,
|
|
620
|
+
velY: hasY ? vel.y : 0,
|
|
621
|
+
velZ: hasZ ? vel.z : 0,
|
|
622
|
+
hasX,
|
|
623
|
+
hasY,
|
|
624
|
+
hasZ,
|
|
625
|
+
stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 200,
|
|
626
|
+
damping: userDamping !== null && userDamping !== void 0 ? userDamping : 50,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
(_b = cur.onDragEnd) === null || _b === void 0 ? void 0 : _b.call(cur, event, {
|
|
631
|
+
point: { x: event.clientX, y: event.clientY },
|
|
632
|
+
offset,
|
|
633
|
+
delta: {
|
|
634
|
+
x: lastDeltaRef.current.x,
|
|
635
|
+
y: lastDeltaRef.current.y,
|
|
636
|
+
z: lastDeltaRef.current.z,
|
|
637
|
+
},
|
|
638
|
+
velocity: { x: vel.x, y: vel.y, z: vel.z },
|
|
639
|
+
});
|
|
640
|
+
};
|
|
641
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
642
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
643
|
+
return () => {
|
|
644
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
645
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
646
|
+
};
|
|
647
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
648
|
+
}, [gl, instanceRef, animateToTarget, buildTargetFromState]);
|
|
649
|
+
const isDragEnabled = drag !== undefined && drag !== false;
|
|
650
|
+
if (!isDragEnabled)
|
|
651
|
+
return {};
|
|
652
|
+
return { onPointerDown: handlePointerDown };
|
|
653
|
+
}
|
|
654
|
+
|
|
245
655
|
function createAnimationState() {
|
|
246
656
|
return {
|
|
247
657
|
hasStarted: false,
|
|
@@ -447,6 +857,7 @@ function custom(Component) {
|
|
|
447
857
|
"attenuationColor",
|
|
448
858
|
]);
|
|
449
859
|
Object.entries(targetValues).forEach(([key, value]) => {
|
|
860
|
+
var _a;
|
|
450
861
|
const opts = getPropertyOpts(key);
|
|
451
862
|
const mapping = transformMap[key];
|
|
452
863
|
if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
|
|
@@ -460,6 +871,14 @@ function custom(Component) {
|
|
|
460
871
|
else if (colorKeys.has(key) && instance[key]) {
|
|
461
872
|
animateColor(instance[key], value, opts, key);
|
|
462
873
|
}
|
|
874
|
+
else if (instance.uniforms &&
|
|
875
|
+
instance.uniforms[key] &&
|
|
876
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
877
|
+
// Animate ShaderMaterial uniforms (uniforms[key].value)
|
|
878
|
+
// Note: consumers must useMemo their uniforms prop to prevent
|
|
879
|
+
// R3F from overwriting animated values on re-render
|
|
880
|
+
createAnimation(instance.uniforms[key], { value: value }, opts, key);
|
|
881
|
+
}
|
|
463
882
|
else if (key in instance && typeof instance[key] === "number") {
|
|
464
883
|
createAnimation(instance, { [key]: value }, opts, key);
|
|
465
884
|
}
|
|
@@ -517,14 +936,20 @@ function custom(Component) {
|
|
|
517
936
|
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
518
937
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
519
938
|
}, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
|
|
939
|
+
const stopAnimation = react.useCallback(() => {
|
|
940
|
+
var _a;
|
|
941
|
+
(_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
942
|
+
}, []);
|
|
520
943
|
const gestureProps = {
|
|
944
|
+
instanceRef,
|
|
521
945
|
captureInstanceState,
|
|
522
946
|
buildTargetFromState,
|
|
523
947
|
animateToTarget,
|
|
524
948
|
resolveVariant,
|
|
525
949
|
transition,
|
|
950
|
+
stopAnimation,
|
|
526
951
|
};
|
|
527
|
-
const gestureHandlers = Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps));
|
|
952
|
+
const gestureHandlers = Object.assign(Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps)), useDrag(false, props, gestureProps));
|
|
528
953
|
const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
|
|
529
954
|
const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
|
|
530
955
|
const getNextChildIndex = react.useCallback(() => {
|
|
@@ -565,11 +990,11 @@ const MotionCamera = (props) => {
|
|
|
565
990
|
const aspect = size.width / size.height;
|
|
566
991
|
let cam;
|
|
567
992
|
if (type === "perspective") {
|
|
568
|
-
cam = new
|
|
993
|
+
cam = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
|
569
994
|
}
|
|
570
995
|
else {
|
|
571
996
|
const frustumSize = 10;
|
|
572
|
-
cam = new
|
|
997
|
+
cam = new THREE.OrthographicCamera((-frustumSize * aspect) / 2, (frustumSize * aspect) / 2, frustumSize / 2, -frustumSize / 2, near, far);
|
|
573
998
|
}
|
|
574
999
|
cam.updateProjectionMatrix();
|
|
575
1000
|
return cam;
|
|
@@ -694,7 +1119,146 @@ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
|
|
|
694
1119
|
});
|
|
695
1120
|
}
|
|
696
1121
|
|
|
1122
|
+
const DEFAULT_SPRING = {
|
|
1123
|
+
type: 'spring',
|
|
1124
|
+
stiffness: 600,
|
|
1125
|
+
damping: 100,
|
|
1126
|
+
restDelta: 0.001,
|
|
1127
|
+
};
|
|
1128
|
+
// Seconds of velocity to project past the release point when snapping —
|
|
1129
|
+
// higher value = a flick advances further.
|
|
1130
|
+
const VELOCITY_PROJECTION = 0.2;
|
|
1131
|
+
// Fraction of slideWidth the user must cross, or minimum velocity (world
|
|
1132
|
+
// units/sec) required, to commit to the next slide. Below both thresholds
|
|
1133
|
+
// the carousel snaps back to the slot the drag started on.
|
|
1134
|
+
const DRAG_THRESHOLD_RATIO = 0.25;
|
|
1135
|
+
const FLICK_VELOCITY = 1.0;
|
|
1136
|
+
const mod = (n, m) => ((n % m) + m) % m;
|
|
1137
|
+
const CarouselSlot = react.memo(({ hadDragRef, item, itemWidth, slotRef }) => {
|
|
1138
|
+
const contentRef = react.useRef(null);
|
|
1139
|
+
const coverRef = react.useRef(null);
|
|
1140
|
+
react.useLayoutEffect(() => {
|
|
1141
|
+
const content = contentRef.current;
|
|
1142
|
+
const cover = coverRef.current;
|
|
1143
|
+
if (!content || !cover)
|
|
1144
|
+
return;
|
|
1145
|
+
const box = new THREE.Box3().setFromObject(content);
|
|
1146
|
+
if (box.isEmpty())
|
|
1147
|
+
return;
|
|
1148
|
+
const h = box.max.y - box.min.y;
|
|
1149
|
+
cover.geometry.dispose();
|
|
1150
|
+
cover.geometry = new THREE.PlaneGeometry(itemWidth, h);
|
|
1151
|
+
cover.position.z = box.max.z + 0.001;
|
|
1152
|
+
}, [item, itemWidth]);
|
|
1153
|
+
// Dispose the generated geometry on unmount — useLayoutEffect's replacement
|
|
1154
|
+
// logic only disposes the *previous* geometry, not the final one.
|
|
1155
|
+
react.useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
|
|
1156
|
+
return (jsxRuntime.jsxs("group", { ref: slotRef, children: [jsxRuntime.jsx("group", { ref: contentRef, children: item }), jsxRuntime.jsxs("mesh", { ref: coverRef, onClick: (e) => {
|
|
1157
|
+
if (hadDragRef.current) {
|
|
1158
|
+
e.stopPropagation();
|
|
1159
|
+
hadDragRef.current = false;
|
|
1160
|
+
}
|
|
1161
|
+
}, children: [jsxRuntime.jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
1162
|
+
});
|
|
1163
|
+
const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold = 1, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY, }) => {
|
|
1164
|
+
const slideWidth = itemWidth + gap;
|
|
1165
|
+
const count = items.length;
|
|
1166
|
+
// Render twice the items and center on the first item of the second copy.
|
|
1167
|
+
// The first copy lives to the left, so wrap-points sit at ±N*slideWidth
|
|
1168
|
+
// from camera center — well off-screen — instead of ±N*slideWidth/2
|
|
1169
|
+
// where they'd pop in and out at the visible edges.
|
|
1170
|
+
const loopedItems = react.useMemo(() => [...items, ...items], [items]);
|
|
1171
|
+
const slotCount = loopedItems.length;
|
|
1172
|
+
const period = slotCount * slideWidth;
|
|
1173
|
+
const startSlot = count + defaultValue;
|
|
1174
|
+
const groupRef = react.useRef(null);
|
|
1175
|
+
const itemRefs = react.useRef([]);
|
|
1176
|
+
// Single source of truth: a fractional slot index that grows up/down
|
|
1177
|
+
// infinitely as the user navigates. group.position.x = -currIndex * slideWidth
|
|
1178
|
+
// at rest. Snap animations animate currIndex toward integer targets;
|
|
1179
|
+
// useFrame applies it to the group every frame (when not dragging).
|
|
1180
|
+
const currIndex = react$1.useMotionValue(startSlot);
|
|
1181
|
+
const isDraggingRef = react.useRef(false);
|
|
1182
|
+
const hadDragRef = react.useRef(false);
|
|
1183
|
+
const dragStartIndexRef = react.useRef(startSlot);
|
|
1184
|
+
const snapAnimRef = react.useRef(null);
|
|
1185
|
+
const initial = react.useMemo(() => ({ x: -startSlot * slideWidth }),
|
|
1186
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1187
|
+
[]);
|
|
1188
|
+
const goTo = react.useCallback((slot) => {
|
|
1189
|
+
var _a;
|
|
1190
|
+
onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
|
|
1191
|
+
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
1192
|
+
snapAnimRef.current = react$1.animate(currIndex, slot, transition);
|
|
1193
|
+
}, [count, transition, onSwitch, currIndex]);
|
|
1194
|
+
react.useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
|
|
1195
|
+
// Drag start: stop any in-flight snap and capture the slot we started on
|
|
1196
|
+
// so an under-threshold release can elastic back to it.
|
|
1197
|
+
const handleDragStart = react.useCallback(() => {
|
|
1198
|
+
var _a;
|
|
1199
|
+
isDraggingRef.current = true;
|
|
1200
|
+
hadDragRef.current = false;
|
|
1201
|
+
dragStartIndexRef.current = Math.round(currIndex.get());
|
|
1202
|
+
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
1203
|
+
snapAnimRef.current = null;
|
|
1204
|
+
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart();
|
|
1205
|
+
}, [currIndex, onDragStart]);
|
|
1206
|
+
const handleDragEnd = react.useCallback((_, info) => {
|
|
1207
|
+
isDraggingRef.current = false;
|
|
1208
|
+
const group = groupRef.current;
|
|
1209
|
+
if (!group)
|
|
1210
|
+
return;
|
|
1211
|
+
// Resync the motion value to where use-drag actually locked the position
|
|
1212
|
+
// — the next useFrame will use this same value, so there's no visible
|
|
1213
|
+
// jump between the dragged position and the start of the snap tween.
|
|
1214
|
+
const releasedX = group.position.x;
|
|
1215
|
+
const releasedIndex = -releasedX / slideWidth;
|
|
1216
|
+
currIndex.jump(releasedIndex);
|
|
1217
|
+
const startIndex = dragStartIndexRef.current;
|
|
1218
|
+
const offsetIndex = releasedIndex - startIndex;
|
|
1219
|
+
const overThreshold = Math.abs(offsetIndex) > dragThreshold;
|
|
1220
|
+
const overVelocity = Math.abs(info.velocity.x) > flickVelocity;
|
|
1221
|
+
if (!overThreshold && !overVelocity) {
|
|
1222
|
+
goTo(startIndex);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
hadDragRef.current = true;
|
|
1226
|
+
const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
|
|
1227
|
+
const targetSlot = Math.round(-projectedX / slideWidth);
|
|
1228
|
+
goTo(targetSlot);
|
|
1229
|
+
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(info);
|
|
1230
|
+
}, [slideWidth, goTo, currIndex, dragThreshold, flickVelocity, onDragEnd]);
|
|
1231
|
+
// Drive position from currIndex unless the user is dragging — during drag
|
|
1232
|
+
// use-drag writes to group.position.x directly (and dragMomentum=false
|
|
1233
|
+
// ensures it stops writing the moment the pointer lifts, so there's no
|
|
1234
|
+
// race with the snap animation).
|
|
1235
|
+
fiber.useFrame(() => {
|
|
1236
|
+
const group = groupRef.current;
|
|
1237
|
+
if (!group)
|
|
1238
|
+
return;
|
|
1239
|
+
if (!isDraggingRef.current) {
|
|
1240
|
+
group.position.x = -currIndex.get() * slideWidth;
|
|
1241
|
+
}
|
|
1242
|
+
const groupX = group.position.x;
|
|
1243
|
+
for (let i = 0; i < slotCount; i++) {
|
|
1244
|
+
const ref = itemRefs.current[i];
|
|
1245
|
+
if (!ref)
|
|
1246
|
+
continue;
|
|
1247
|
+
const k = Math.round((-groupX - i * slideWidth) / period);
|
|
1248
|
+
ref.position.x = i * slideWidth + k * period;
|
|
1249
|
+
// +0.5 margin so the leading-edge slot fades in the moment the trailing
|
|
1250
|
+
// one fades out — keeps the loop seamless mid-drag instead of waiting
|
|
1251
|
+
// for snap to settle on an integer slot.
|
|
1252
|
+
const slotDist = (groupX + ref.position.x) / slideWidth;
|
|
1253
|
+
ref.visible = Math.abs(slotDist) <= renderThreshold + 0.5;
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
return (jsxRuntime.jsx(motion.group, { ref: groupRef, drag: "x", dragMomentum: false, initial: initial, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => (jsxRuntime.jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, hadDragRef: hadDragRef }, `carouselItem-${i}`))) }));
|
|
1257
|
+
};
|
|
1258
|
+
var index = react.memo(Carousel);
|
|
1259
|
+
|
|
697
1260
|
exports.AnimatePresence = AnimatePresence;
|
|
1261
|
+
exports.Carousel = index;
|
|
698
1262
|
exports.MotionCamera = MotionCamera;
|
|
699
1263
|
exports.motion = motion;
|
|
700
1264
|
exports.usePresence = usePresence;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { memo, useMemo, useRef, useCallback, useEffect, useLayoutEffect } from 'react';
|
|
4
|
+
import { useFrame } from '@react-three/fiber';
|
|
5
|
+
import { Box3, PlaneGeometry } from 'three';
|
|
6
|
+
import { useMotionValue, animate } from 'motion/react';
|
|
7
|
+
import { motion } from '../../render/motion.mjs';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SPRING = {
|
|
10
|
+
type: 'spring',
|
|
11
|
+
stiffness: 600,
|
|
12
|
+
damping: 100,
|
|
13
|
+
restDelta: 0.001,
|
|
14
|
+
};
|
|
15
|
+
// Seconds of velocity to project past the release point when snapping —
|
|
16
|
+
// higher value = a flick advances further.
|
|
17
|
+
const VELOCITY_PROJECTION = 0.2;
|
|
18
|
+
// Fraction of slideWidth the user must cross, or minimum velocity (world
|
|
19
|
+
// units/sec) required, to commit to the next slide. Below both thresholds
|
|
20
|
+
// the carousel snaps back to the slot the drag started on.
|
|
21
|
+
const DRAG_THRESHOLD_RATIO = 0.25;
|
|
22
|
+
const FLICK_VELOCITY = 1.0;
|
|
23
|
+
const mod = (n, m) => ((n % m) + m) % m;
|
|
24
|
+
const CarouselSlot = memo(({ hadDragRef, item, itemWidth, slotRef }) => {
|
|
25
|
+
const contentRef = useRef(null);
|
|
26
|
+
const coverRef = useRef(null);
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
const content = contentRef.current;
|
|
29
|
+
const cover = coverRef.current;
|
|
30
|
+
if (!content || !cover)
|
|
31
|
+
return;
|
|
32
|
+
const box = new Box3().setFromObject(content);
|
|
33
|
+
if (box.isEmpty())
|
|
34
|
+
return;
|
|
35
|
+
const h = box.max.y - box.min.y;
|
|
36
|
+
cover.geometry.dispose();
|
|
37
|
+
cover.geometry = new PlaneGeometry(itemWidth, h);
|
|
38
|
+
cover.position.z = box.max.z + 0.001;
|
|
39
|
+
}, [item, itemWidth]);
|
|
40
|
+
// Dispose the generated geometry on unmount — useLayoutEffect's replacement
|
|
41
|
+
// logic only disposes the *previous* geometry, not the final one.
|
|
42
|
+
useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
|
|
43
|
+
return (jsxs("group", { ref: slotRef, children: [jsx("group", { ref: contentRef, children: item }), jsxs("mesh", { ref: coverRef, onClick: (e) => {
|
|
44
|
+
if (hadDragRef.current) {
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
hadDragRef.current = false;
|
|
47
|
+
}
|
|
48
|
+
}, children: [jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
49
|
+
});
|
|
50
|
+
const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold = 1, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY, }) => {
|
|
51
|
+
const slideWidth = itemWidth + gap;
|
|
52
|
+
const count = items.length;
|
|
53
|
+
// Render twice the items and center on the first item of the second copy.
|
|
54
|
+
// The first copy lives to the left, so wrap-points sit at ±N*slideWidth
|
|
55
|
+
// from camera center — well off-screen — instead of ±N*slideWidth/2
|
|
56
|
+
// where they'd pop in and out at the visible edges.
|
|
57
|
+
const loopedItems = useMemo(() => [...items, ...items], [items]);
|
|
58
|
+
const slotCount = loopedItems.length;
|
|
59
|
+
const period = slotCount * slideWidth;
|
|
60
|
+
const startSlot = count + defaultValue;
|
|
61
|
+
const groupRef = useRef(null);
|
|
62
|
+
const itemRefs = useRef([]);
|
|
63
|
+
// Single source of truth: a fractional slot index that grows up/down
|
|
64
|
+
// infinitely as the user navigates. group.position.x = -currIndex * slideWidth
|
|
65
|
+
// at rest. Snap animations animate currIndex toward integer targets;
|
|
66
|
+
// useFrame applies it to the group every frame (when not dragging).
|
|
67
|
+
const currIndex = useMotionValue(startSlot);
|
|
68
|
+
const isDraggingRef = useRef(false);
|
|
69
|
+
const hadDragRef = useRef(false);
|
|
70
|
+
const dragStartIndexRef = useRef(startSlot);
|
|
71
|
+
const snapAnimRef = useRef(null);
|
|
72
|
+
const initial = useMemo(() => ({ x: -startSlot * slideWidth }),
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
[]);
|
|
75
|
+
const goTo = useCallback((slot) => {
|
|
76
|
+
var _a;
|
|
77
|
+
onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
|
|
78
|
+
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
79
|
+
snapAnimRef.current = animate(currIndex, slot, transition);
|
|
80
|
+
}, [count, transition, onSwitch, currIndex]);
|
|
81
|
+
useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
|
|
82
|
+
// Drag start: stop any in-flight snap and capture the slot we started on
|
|
83
|
+
// so an under-threshold release can elastic back to it.
|
|
84
|
+
const handleDragStart = useCallback(() => {
|
|
85
|
+
var _a;
|
|
86
|
+
isDraggingRef.current = true;
|
|
87
|
+
hadDragRef.current = false;
|
|
88
|
+
dragStartIndexRef.current = Math.round(currIndex.get());
|
|
89
|
+
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
90
|
+
snapAnimRef.current = null;
|
|
91
|
+
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart();
|
|
92
|
+
}, [currIndex, onDragStart]);
|
|
93
|
+
const handleDragEnd = useCallback((_, info) => {
|
|
94
|
+
isDraggingRef.current = false;
|
|
95
|
+
const group = groupRef.current;
|
|
96
|
+
if (!group)
|
|
97
|
+
return;
|
|
98
|
+
// Resync the motion value to where use-drag actually locked the position
|
|
99
|
+
// — the next useFrame will use this same value, so there's no visible
|
|
100
|
+
// jump between the dragged position and the start of the snap tween.
|
|
101
|
+
const releasedX = group.position.x;
|
|
102
|
+
const releasedIndex = -releasedX / slideWidth;
|
|
103
|
+
currIndex.jump(releasedIndex);
|
|
104
|
+
const startIndex = dragStartIndexRef.current;
|
|
105
|
+
const offsetIndex = releasedIndex - startIndex;
|
|
106
|
+
const overThreshold = Math.abs(offsetIndex) > dragThreshold;
|
|
107
|
+
const overVelocity = Math.abs(info.velocity.x) > flickVelocity;
|
|
108
|
+
if (!overThreshold && !overVelocity) {
|
|
109
|
+
goTo(startIndex);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
hadDragRef.current = true;
|
|
113
|
+
const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
|
|
114
|
+
const targetSlot = Math.round(-projectedX / slideWidth);
|
|
115
|
+
goTo(targetSlot);
|
|
116
|
+
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(info);
|
|
117
|
+
}, [slideWidth, goTo, currIndex, dragThreshold, flickVelocity, onDragEnd]);
|
|
118
|
+
// Drive position from currIndex unless the user is dragging — during drag
|
|
119
|
+
// use-drag writes to group.position.x directly (and dragMomentum=false
|
|
120
|
+
// ensures it stops writing the moment the pointer lifts, so there's no
|
|
121
|
+
// race with the snap animation).
|
|
122
|
+
useFrame(() => {
|
|
123
|
+
const group = groupRef.current;
|
|
124
|
+
if (!group)
|
|
125
|
+
return;
|
|
126
|
+
if (!isDraggingRef.current) {
|
|
127
|
+
group.position.x = -currIndex.get() * slideWidth;
|
|
128
|
+
}
|
|
129
|
+
const groupX = group.position.x;
|
|
130
|
+
for (let i = 0; i < slotCount; i++) {
|
|
131
|
+
const ref = itemRefs.current[i];
|
|
132
|
+
if (!ref)
|
|
133
|
+
continue;
|
|
134
|
+
const k = Math.round((-groupX - i * slideWidth) / period);
|
|
135
|
+
ref.position.x = i * slideWidth + k * period;
|
|
136
|
+
// +0.5 margin so the leading-edge slot fades in the moment the trailing
|
|
137
|
+
// one fades out — keeps the loop seamless mid-drag instead of waiting
|
|
138
|
+
// for snap to settle on an integer slot.
|
|
139
|
+
const slotDist = (groupX + ref.position.x) / slideWidth;
|
|
140
|
+
ref.visible = Math.abs(slotDist) <= renderThreshold + 0.5;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return (jsx(motion.group, { ref: groupRef, drag: "x", dragMomentum: false, initial: initial, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => (jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, hadDragRef: hadDragRef }, `carouselItem-${i}`))) }));
|
|
144
|
+
};
|
|
145
|
+
var index = memo(Carousel);
|
|
146
|
+
|
|
147
|
+
export { index as default };
|
package/dist/es/index.mjs
CHANGED
|
@@ -3,3 +3,4 @@ export { motion } from './render/motion.mjs';
|
|
|
3
3
|
export { default as MotionCamera } from './components/MotionCamera.mjs';
|
|
4
4
|
export { AnimatePresence } from './components/AnimatePresence/index.mjs';
|
|
5
5
|
export { usePresence } from './components/AnimatePresence/PresenceContext.mjs';
|
|
6
|
+
export { default as Carousel } from './components/Carousel/index.mjs';
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
|
|
6
|
+
// World-space units per CSS pixel at the given depth from the camera.
|
|
7
|
+
const getWorldPerPixel = (camera, depth, viewportHeight) => {
|
|
8
|
+
var _a;
|
|
9
|
+
const ortho = camera;
|
|
10
|
+
if (ortho.isOrthographicCamera) {
|
|
11
|
+
return ((ortho.top - ortho.bottom) / (viewportHeight * (ortho.zoom || 1)));
|
|
12
|
+
}
|
|
13
|
+
const persp = camera;
|
|
14
|
+
const fov = ((_a = persp.fov) !== null && _a !== void 0 ? _a : 60) * (Math.PI / 180);
|
|
15
|
+
return (2 * Math.tan(fov / 2) * depth) / viewportHeight;
|
|
16
|
+
};
|
|
17
|
+
const clampWithElastic = (value, min, max, elastic) => {
|
|
18
|
+
if (min !== undefined && value < min)
|
|
19
|
+
return min + (value - min) * elastic;
|
|
20
|
+
if (max !== undefined && value > max)
|
|
21
|
+
return max + (value - max) * elastic;
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
function useDrag(isStatic, props, options) {
|
|
25
|
+
const { camera, gl } = useThree();
|
|
26
|
+
const { drag, whileDrag, onDragStart, onPointerDown, transition } = props;
|
|
27
|
+
const { instanceRef, captureInstanceState, buildTargetFromState, animateToTarget, resolveVariant, stopAnimation, } = options;
|
|
28
|
+
const isDraggingRef = useRef(false);
|
|
29
|
+
const dragStartPointerRef = useRef({ x: 0, y: 0 });
|
|
30
|
+
const dragStartPosRef = useRef(new THREE.Vector3());
|
|
31
|
+
const cameraRightRef = useRef(new THREE.Vector3());
|
|
32
|
+
const cameraUpRef = useRef(new THREE.Vector3());
|
|
33
|
+
const worldPerPixelRef = useRef(0);
|
|
34
|
+
const targetPosRef = useRef(new THREE.Vector3());
|
|
35
|
+
const lastPosRef = useRef(new THREE.Vector3());
|
|
36
|
+
const velocityRef = useRef(new THREE.Vector3());
|
|
37
|
+
const lastDeltaRef = useRef(new THREE.Vector3());
|
|
38
|
+
const lastTimeRef = useRef(0);
|
|
39
|
+
const preDragStateRef = useRef(null);
|
|
40
|
+
const pointerIdRef = useRef(null);
|
|
41
|
+
const springStateRef = useRef(null);
|
|
42
|
+
const lastFrameTimeRef = useRef(0);
|
|
43
|
+
const propsRef = useRef(props);
|
|
44
|
+
propsRef.current = props;
|
|
45
|
+
useFrame(() => {
|
|
46
|
+
var _a;
|
|
47
|
+
const instance = instanceRef.current;
|
|
48
|
+
if (!instance)
|
|
49
|
+
return;
|
|
50
|
+
const obj = instance;
|
|
51
|
+
// Phase 1: dragging — apply the drag target
|
|
52
|
+
if (isDraggingRef.current) {
|
|
53
|
+
const t = targetPosRef.current;
|
|
54
|
+
obj.position.set(t.x, t.y, t.z);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Phase 2: spring (momentum / snap-to-origin)
|
|
58
|
+
const s = springStateRef.current;
|
|
59
|
+
if (!s || !s.active)
|
|
60
|
+
return;
|
|
61
|
+
const now = performance.now();
|
|
62
|
+
let dt = (now - lastFrameTimeRef.current) / 1000;
|
|
63
|
+
if (dt > 0.05)
|
|
64
|
+
dt = 0.05;
|
|
65
|
+
if (dt <= 0)
|
|
66
|
+
dt = 1 / 60;
|
|
67
|
+
lastFrameTimeRef.current = now;
|
|
68
|
+
// Sub-step the integrator for stability with stiff springs
|
|
69
|
+
const subSteps = 4;
|
|
70
|
+
const subDt = dt / subSteps;
|
|
71
|
+
for (let i = 0; i < subSteps; i++) {
|
|
72
|
+
if (s.hasX) {
|
|
73
|
+
const force = -s.stiffness * (obj.position.x - s.targetX) - s.damping * s.velX;
|
|
74
|
+
s.velX += force * subDt;
|
|
75
|
+
obj.position.x += s.velX * subDt;
|
|
76
|
+
}
|
|
77
|
+
if (s.hasY) {
|
|
78
|
+
const force = -s.stiffness * (obj.position.y - s.targetY) - s.damping * s.velY;
|
|
79
|
+
s.velY += force * subDt;
|
|
80
|
+
obj.position.y += s.velY * subDt;
|
|
81
|
+
}
|
|
82
|
+
if (s.hasZ) {
|
|
83
|
+
const force = -s.stiffness * (obj.position.z - s.targetZ) - s.damping * s.velZ;
|
|
84
|
+
s.velZ += force * subDt;
|
|
85
|
+
obj.position.z += s.velZ * subDt;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Rest detection — stop when both speed and offset are tiny
|
|
89
|
+
const speedSq = s.velX * s.velX + s.velY * s.velY + s.velZ * s.velZ;
|
|
90
|
+
const offX = s.hasX ? obj.position.x - s.targetX : 0;
|
|
91
|
+
const offY = s.hasY ? obj.position.y - s.targetY : 0;
|
|
92
|
+
const offZ = s.hasZ ? obj.position.z - s.targetZ : 0;
|
|
93
|
+
const offsetSq = offX * offX + offY * offY + offZ * offZ;
|
|
94
|
+
if (speedSq < 0.0005 && offsetSq < 0.0005) {
|
|
95
|
+
if (s.hasX)
|
|
96
|
+
obj.position.x = s.targetX;
|
|
97
|
+
if (s.hasY)
|
|
98
|
+
obj.position.y = s.targetY;
|
|
99
|
+
if (s.hasZ)
|
|
100
|
+
obj.position.z = s.targetZ;
|
|
101
|
+
s.active = false;
|
|
102
|
+
(_a = s.onComplete) === null || _a === void 0 ? void 0 : _a.call(s);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const handlePointerDown = useCallback((event) => {
|
|
106
|
+
const instance = instanceRef.current;
|
|
107
|
+
if (!instance)
|
|
108
|
+
return;
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
// Halt any in-flight motion-library animation (e.g. main `animate` prop
|
|
111
|
+
// tween that's still running) and any active drag spring.
|
|
112
|
+
stopAnimation();
|
|
113
|
+
if (springStateRef.current)
|
|
114
|
+
springStateRef.current.active = false;
|
|
115
|
+
if (whileDrag) {
|
|
116
|
+
preDragStateRef.current = captureInstanceState();
|
|
117
|
+
const targetValues = typeof whileDrag === "string"
|
|
118
|
+
? resolveVariant(whileDrag)
|
|
119
|
+
: whileDrag;
|
|
120
|
+
animateToTarget(targetValues, transition || { duration: 0.1 });
|
|
121
|
+
}
|
|
122
|
+
const obj = instance;
|
|
123
|
+
// Capture initial pointer + position. All subsequent moves are computed
|
|
124
|
+
// as `initialPos + pixelDelta * worldPerPixel` — never re-derived from
|
|
125
|
+
// the live (potentially-mid-animation) instance position.
|
|
126
|
+
dragStartPointerRef.current.x = event.nativeEvent.clientX;
|
|
127
|
+
dragStartPointerRef.current.y = event.nativeEvent.clientY;
|
|
128
|
+
dragStartPosRef.current.set(obj.position.x, obj.position.y, obj.position.z);
|
|
129
|
+
targetPosRef.current.copy(dragStartPosRef.current);
|
|
130
|
+
lastPosRef.current.copy(dragStartPosRef.current);
|
|
131
|
+
lastDeltaRef.current.set(0, 0, 0);
|
|
132
|
+
velocityRef.current.set(0, 0, 0);
|
|
133
|
+
lastTimeRef.current = performance.now();
|
|
134
|
+
// Compute world-units-per-pixel at the object's perpendicular depth
|
|
135
|
+
// (distance from camera along its forward axis).
|
|
136
|
+
const cameraForward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
|
137
|
+
const cameraToObj = new THREE.Vector3().subVectors(dragStartPosRef.current, camera.position);
|
|
138
|
+
const depth = Math.abs(cameraToObj.dot(cameraForward));
|
|
139
|
+
const rect = gl.domElement.getBoundingClientRect();
|
|
140
|
+
worldPerPixelRef.current = getWorldPerPixel(camera, depth, rect.height);
|
|
141
|
+
// Cache camera basis vectors so screen X/Y maps correctly to world
|
|
142
|
+
// X/Y/Z even if the camera is tilted.
|
|
143
|
+
cameraRightRef.current.set(1, 0, 0).applyQuaternion(camera.quaternion);
|
|
144
|
+
cameraUpRef.current.set(0, 1, 0).applyQuaternion(camera.quaternion);
|
|
145
|
+
isDraggingRef.current = true;
|
|
146
|
+
pointerIdRef.current = event.nativeEvent.pointerId;
|
|
147
|
+
try {
|
|
148
|
+
gl.domElement.setPointerCapture(event.nativeEvent.pointerId);
|
|
149
|
+
}
|
|
150
|
+
catch (_a) {
|
|
151
|
+
// not all environments support setPointerCapture
|
|
152
|
+
}
|
|
153
|
+
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(event.nativeEvent, {
|
|
154
|
+
point: { x: event.nativeEvent.clientX, y: event.nativeEvent.clientY },
|
|
155
|
+
offset: { x: 0, y: 0, z: 0 },
|
|
156
|
+
delta: { x: 0, y: 0, z: 0 },
|
|
157
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
158
|
+
});
|
|
159
|
+
onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
|
|
160
|
+
},
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
162
|
+
[
|
|
163
|
+
whileDrag,
|
|
164
|
+
onDragStart,
|
|
165
|
+
onPointerDown,
|
|
166
|
+
camera,
|
|
167
|
+
gl,
|
|
168
|
+
captureInstanceState,
|
|
169
|
+
resolveVariant,
|
|
170
|
+
animateToTarget,
|
|
171
|
+
stopAnimation,
|
|
172
|
+
transition,
|
|
173
|
+
instanceRef,
|
|
174
|
+
]);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const handlePointerMove = (event) => {
|
|
177
|
+
var _a;
|
|
178
|
+
if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
|
|
179
|
+
return;
|
|
180
|
+
const instance = instanceRef.current;
|
|
181
|
+
if (!instance)
|
|
182
|
+
return;
|
|
183
|
+
const cur = propsRef.current;
|
|
184
|
+
const dragAxis = cur.drag;
|
|
185
|
+
const elasticVal = typeof cur.dragElastic === "number"
|
|
186
|
+
? cur.dragElastic
|
|
187
|
+
: cur.dragElastic === false
|
|
188
|
+
? 0
|
|
189
|
+
: 0.5;
|
|
190
|
+
const constraints = cur.dragConstraints;
|
|
191
|
+
const pixelDx = event.clientX - dragStartPointerRef.current.x;
|
|
192
|
+
const pixelDy = event.clientY - dragStartPointerRef.current.y;
|
|
193
|
+
const scale = worldPerPixelRef.current;
|
|
194
|
+
let newX = dragStartPosRef.current.x;
|
|
195
|
+
let newY = dragStartPosRef.current.y;
|
|
196
|
+
let newZ = dragStartPosRef.current.z;
|
|
197
|
+
if (dragAxis === "z") {
|
|
198
|
+
// Vertical screen movement → Z. Cursor down (pixelDy > 0) = closer.
|
|
199
|
+
newZ = dragStartPosRef.current.z + pixelDy * scale;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Map screen XY to camera-relative world delta.
|
|
203
|
+
const r = cameraRightRef.current;
|
|
204
|
+
const u = cameraUpRef.current;
|
|
205
|
+
const worldDx = r.x * pixelDx * scale + u.x * -pixelDy * scale;
|
|
206
|
+
const worldDy = r.y * pixelDx * scale + u.y * -pixelDy * scale;
|
|
207
|
+
const worldDz = r.z * pixelDx * scale + u.z * -pixelDy * scale;
|
|
208
|
+
if (dragAxis === "x") {
|
|
209
|
+
newX = dragStartPosRef.current.x + worldDx;
|
|
210
|
+
}
|
|
211
|
+
else if (dragAxis === "y") {
|
|
212
|
+
newY = dragStartPosRef.current.y + worldDy;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
newX = dragStartPosRef.current.x + worldDx;
|
|
216
|
+
newY = dragStartPosRef.current.y + worldDy;
|
|
217
|
+
newZ = dragStartPosRef.current.z + worldDz;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Constraints with elastic rubber-banding
|
|
221
|
+
if (constraints) {
|
|
222
|
+
if (dragAxis !== "y" && dragAxis !== "z") {
|
|
223
|
+
newX = clampWithElastic(newX, constraints.left, constraints.right, elasticVal);
|
|
224
|
+
}
|
|
225
|
+
if (dragAxis !== "x" && dragAxis !== "z") {
|
|
226
|
+
newY = clampWithElastic(newY, constraints.bottom, constraints.top, elasticVal);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Velocity tracking (used for momentum on release)
|
|
230
|
+
const now = performance.now();
|
|
231
|
+
const dt = (now - lastTimeRef.current) / 1000;
|
|
232
|
+
if (dt > 0) {
|
|
233
|
+
const dx = newX - lastPosRef.current.x;
|
|
234
|
+
const dy = newY - lastPosRef.current.y;
|
|
235
|
+
const dz = newZ - lastPosRef.current.z;
|
|
236
|
+
velocityRef.current.set(dx / dt, dy / dt, dz / dt);
|
|
237
|
+
lastDeltaRef.current.set(dx, dy, dz);
|
|
238
|
+
}
|
|
239
|
+
lastTimeRef.current = now;
|
|
240
|
+
lastPosRef.current.set(newX, newY, newZ);
|
|
241
|
+
// Stash target — useFrame applies it next R3F frame.
|
|
242
|
+
targetPosRef.current.set(newX, newY, newZ);
|
|
243
|
+
const offset = {
|
|
244
|
+
x: newX - dragStartPosRef.current.x,
|
|
245
|
+
y: newY - dragStartPosRef.current.y,
|
|
246
|
+
z: newZ - dragStartPosRef.current.z,
|
|
247
|
+
};
|
|
248
|
+
(_a = cur.onDrag) === null || _a === void 0 ? void 0 : _a.call(cur, event, {
|
|
249
|
+
point: { x: event.clientX, y: event.clientY },
|
|
250
|
+
offset,
|
|
251
|
+
delta: {
|
|
252
|
+
x: lastDeltaRef.current.x,
|
|
253
|
+
y: lastDeltaRef.current.y,
|
|
254
|
+
z: lastDeltaRef.current.z,
|
|
255
|
+
},
|
|
256
|
+
velocity: {
|
|
257
|
+
x: velocityRef.current.x,
|
|
258
|
+
y: velocityRef.current.y,
|
|
259
|
+
z: velocityRef.current.z,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
const handlePointerUp = (event) => {
|
|
264
|
+
var _a, _b;
|
|
265
|
+
if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
|
|
266
|
+
return;
|
|
267
|
+
isDraggingRef.current = false;
|
|
268
|
+
pointerIdRef.current = null;
|
|
269
|
+
const instance = instanceRef.current;
|
|
270
|
+
const cur = propsRef.current;
|
|
271
|
+
// Lock in the final dragged position synchronously. Once useFrame stops
|
|
272
|
+
// applying the drag target, a still-pending write from the *previous*
|
|
273
|
+
// momentum animation can land before the new animateToTarget() call
|
|
274
|
+
// reads instance.position to use as its "from" value — which causes the
|
|
275
|
+
// new animation to start from the old momentum's path instead of where
|
|
276
|
+
// the user actually released.
|
|
277
|
+
if (instance) {
|
|
278
|
+
const obj = instance;
|
|
279
|
+
const t = targetPosRef.current;
|
|
280
|
+
obj.position.set(t.x, t.y, t.z);
|
|
281
|
+
}
|
|
282
|
+
// Restore whileDrag visual state
|
|
283
|
+
if (cur.whileDrag && preDragStateRef.current) {
|
|
284
|
+
const targetValues = buildTargetFromState(preDragStateRef.current);
|
|
285
|
+
animateToTarget(targetValues, options.transition || { duration: 0.2 });
|
|
286
|
+
const dur = ((_a = options.transition) === null || _a === void 0 ? void 0 : _a.duration) || 0.2;
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
preDragStateRef.current = null;
|
|
289
|
+
}, dur * 1000);
|
|
290
|
+
}
|
|
291
|
+
if (!instance)
|
|
292
|
+
return;
|
|
293
|
+
const finalPos = targetPosRef.current;
|
|
294
|
+
const offset = {
|
|
295
|
+
x: finalPos.x - dragStartPosRef.current.x,
|
|
296
|
+
y: finalPos.y - dragStartPosRef.current.y,
|
|
297
|
+
z: finalPos.z - dragStartPosRef.current.z,
|
|
298
|
+
};
|
|
299
|
+
const vel = velocityRef.current;
|
|
300
|
+
// Read spring tuning from dragTransition (if user provided one).
|
|
301
|
+
const dt = cur.dragTransition;
|
|
302
|
+
const userStiffness = typeof (dt === null || dt === void 0 ? void 0 : dt.stiffness) === "number" ? dt.stiffness : undefined;
|
|
303
|
+
const userDamping = typeof (dt === null || dt === void 0 ? void 0 : dt.damping) === "number" ? dt.damping : undefined;
|
|
304
|
+
if (cur.dragSnapToOrigin) {
|
|
305
|
+
lastFrameTimeRef.current = performance.now();
|
|
306
|
+
springStateRef.current = {
|
|
307
|
+
active: true,
|
|
308
|
+
targetX: dragStartPosRef.current.x,
|
|
309
|
+
targetY: dragStartPosRef.current.y,
|
|
310
|
+
targetZ: dragStartPosRef.current.z,
|
|
311
|
+
velX: vel.x,
|
|
312
|
+
velY: vel.y,
|
|
313
|
+
velZ: vel.z,
|
|
314
|
+
hasX: true,
|
|
315
|
+
hasY: true,
|
|
316
|
+
hasZ: true,
|
|
317
|
+
stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 400,
|
|
318
|
+
damping: userDamping !== null && userDamping !== void 0 ? userDamping : 40,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
else if (cur.dragMomentum !== false) {
|
|
322
|
+
const speed = Math.sqrt(Math.pow(vel.x, 2) + Math.pow(vel.y, 2) + Math.pow(vel.z, 2));
|
|
323
|
+
if (speed > 0.5) {
|
|
324
|
+
const coeff = 0.25;
|
|
325
|
+
const dragAxis = cur.drag;
|
|
326
|
+
const hasX = dragAxis !== "y" && dragAxis !== "z";
|
|
327
|
+
const hasY = dragAxis !== "x" && dragAxis !== "z";
|
|
328
|
+
const hasZ = dragAxis === "z";
|
|
329
|
+
let targetX = hasX ? finalPos.x + vel.x * coeff : finalPos.x;
|
|
330
|
+
let targetY = hasY ? finalPos.y + vel.y * coeff : finalPos.y;
|
|
331
|
+
const targetZ = hasZ ? finalPos.z + vel.z * coeff : finalPos.z;
|
|
332
|
+
const constraints = cur.dragConstraints;
|
|
333
|
+
if (constraints) {
|
|
334
|
+
if (hasX) {
|
|
335
|
+
if (constraints.left !== undefined)
|
|
336
|
+
targetX = Math.max(targetX, constraints.left);
|
|
337
|
+
if (constraints.right !== undefined)
|
|
338
|
+
targetX = Math.min(targetX, constraints.right);
|
|
339
|
+
}
|
|
340
|
+
if (hasY) {
|
|
341
|
+
if (constraints.bottom !== undefined)
|
|
342
|
+
targetY = Math.max(targetY, constraints.bottom);
|
|
343
|
+
if (constraints.top !== undefined)
|
|
344
|
+
targetY = Math.min(targetY, constraints.top);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
lastFrameTimeRef.current = performance.now();
|
|
348
|
+
springStateRef.current = {
|
|
349
|
+
active: true,
|
|
350
|
+
targetX,
|
|
351
|
+
targetY,
|
|
352
|
+
targetZ,
|
|
353
|
+
velX: hasX ? vel.x : 0,
|
|
354
|
+
velY: hasY ? vel.y : 0,
|
|
355
|
+
velZ: hasZ ? vel.z : 0,
|
|
356
|
+
hasX,
|
|
357
|
+
hasY,
|
|
358
|
+
hasZ,
|
|
359
|
+
stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 200,
|
|
360
|
+
damping: userDamping !== null && userDamping !== void 0 ? userDamping : 50,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
(_b = cur.onDragEnd) === null || _b === void 0 ? void 0 : _b.call(cur, event, {
|
|
365
|
+
point: { x: event.clientX, y: event.clientY },
|
|
366
|
+
offset,
|
|
367
|
+
delta: {
|
|
368
|
+
x: lastDeltaRef.current.x,
|
|
369
|
+
y: lastDeltaRef.current.y,
|
|
370
|
+
z: lastDeltaRef.current.z,
|
|
371
|
+
},
|
|
372
|
+
velocity: { x: vel.x, y: vel.y, z: vel.z },
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
376
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
377
|
+
return () => {
|
|
378
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
379
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
380
|
+
};
|
|
381
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
382
|
+
}, [gl, instanceRef, animateToTarget, buildTargetFromState]);
|
|
383
|
+
const isDragEnabled = drag !== undefined && drag !== false;
|
|
384
|
+
if (!isDragEnabled)
|
|
385
|
+
return {};
|
|
386
|
+
return { onPointerDown: handlePointerDown };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export { useDrag };
|
|
@@ -6,6 +6,7 @@ import { PresenceContext } from '../components/AnimatePresence/PresenceContext.m
|
|
|
6
6
|
import { useRender } from './use-render.mjs';
|
|
7
7
|
import { useHover } from './gestures/use-hover.mjs';
|
|
8
8
|
import { useTap } from './gestures/use-tap.mjs';
|
|
9
|
+
import { useDrag } from './gestures/use-drag.mjs';
|
|
9
10
|
import { createAnimationState, createCallbackOptions, registerAnimation } from './events/index.mjs';
|
|
10
11
|
|
|
11
12
|
const MotionContext = createContext(null);
|
|
@@ -166,6 +167,7 @@ function custom(Component) {
|
|
|
166
167
|
"attenuationColor",
|
|
167
168
|
]);
|
|
168
169
|
Object.entries(targetValues).forEach(([key, value]) => {
|
|
170
|
+
var _a;
|
|
169
171
|
const opts = getPropertyOpts(key);
|
|
170
172
|
const mapping = transformMap[key];
|
|
171
173
|
if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
|
|
@@ -179,6 +181,14 @@ function custom(Component) {
|
|
|
179
181
|
else if (colorKeys.has(key) && instance[key]) {
|
|
180
182
|
animateColor(instance[key], value, opts, key);
|
|
181
183
|
}
|
|
184
|
+
else if (instance.uniforms &&
|
|
185
|
+
instance.uniforms[key] &&
|
|
186
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
187
|
+
// Animate ShaderMaterial uniforms (uniforms[key].value)
|
|
188
|
+
// Note: consumers must useMemo their uniforms prop to prevent
|
|
189
|
+
// R3F from overwriting animated values on re-render
|
|
190
|
+
createAnimation(instance.uniforms[key], { value: value }, opts, key);
|
|
191
|
+
}
|
|
182
192
|
else if (key in instance && typeof instance[key] === "number") {
|
|
183
193
|
createAnimation(instance, { [key]: value }, opts, key);
|
|
184
194
|
}
|
|
@@ -236,14 +246,20 @@ function custom(Component) {
|
|
|
236
246
|
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
237
247
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
248
|
}, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
|
|
249
|
+
const stopAnimation = useCallback(() => {
|
|
250
|
+
var _a;
|
|
251
|
+
(_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
252
|
+
}, []);
|
|
239
253
|
const gestureProps = {
|
|
254
|
+
instanceRef,
|
|
240
255
|
captureInstanceState,
|
|
241
256
|
buildTargetFromState,
|
|
242
257
|
animateToTarget,
|
|
243
258
|
resolveVariant,
|
|
244
259
|
transition,
|
|
260
|
+
stopAnimation,
|
|
245
261
|
};
|
|
246
|
-
const gestureHandlers = Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps));
|
|
262
|
+
const gestureHandlers = Object.assign(Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps)), useDrag(false, props, gestureProps));
|
|
247
263
|
const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
|
|
248
264
|
const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
|
|
249
265
|
const getNextChildIndex = useCallback(() => {
|
|
@@ -7,6 +7,7 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
|
|
|
7
7
|
* Create a callback ref that captures the Three.js instance
|
|
8
8
|
*/
|
|
9
9
|
const callbackRef = useCallback((instance) => {
|
|
10
|
+
var _a;
|
|
10
11
|
if (!instance)
|
|
11
12
|
return;
|
|
12
13
|
// Apply initial values immediately to prevent FOUC - but only once
|
|
@@ -45,6 +46,12 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
|
|
|
45
46
|
colorProp.set(initialValues[key]);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
49
|
+
else if (instance.uniforms &&
|
|
50
|
+
instance.uniforms[key] &&
|
|
51
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
52
|
+
// Set ShaderMaterial uniform initial value
|
|
53
|
+
instance.uniforms[key].value = initialValues[key];
|
|
54
|
+
}
|
|
48
55
|
else if (key in instance && typeof instance[key] === "number") {
|
|
49
56
|
instance[key] = initialValues[key];
|
|
50
57
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,12 +2,49 @@ import { Vector3, Euler, Color, ReactThreeFiber } from '@react-three/fiber';
|
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, ReactNode } from 'react';
|
|
5
|
-
import { MotionValue, MotionProps, ResolvedValues } from 'motion/react';
|
|
5
|
+
import { MotionValue, MotionProps, Transition, ResolvedValues } from 'motion/react';
|
|
6
6
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
7
7
|
|
|
8
8
|
type ThreeElement = InstanceType<typeof THREE.Object3D> & Record<string, unknown>;
|
|
9
|
-
interface
|
|
9
|
+
interface DragInfo {
|
|
10
|
+
point: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
offset: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
z: number;
|
|
18
|
+
};
|
|
19
|
+
delta: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
z: number;
|
|
23
|
+
};
|
|
24
|
+
velocity: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
z: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface DragConstraints {
|
|
31
|
+
left?: number;
|
|
32
|
+
right?: number;
|
|
33
|
+
top?: number;
|
|
34
|
+
bottom?: number;
|
|
35
|
+
}
|
|
36
|
+
interface ThreeMotionProps extends Omit<MotionProps, "style" | "children" | "onUpdate" | "onAnimationStart" | "onAnimationComplete" | "drag" | "dragConstraints" | "dragElastic" | "dragMomentum" | "dragSnapToOrigin" | "dragTransition" | "onDrag" | "onDragStart" | "onDragEnd" | "whileDrag" | "dragDirectionLock" | "dragPropagation" | "dragListener"> {
|
|
10
37
|
[key: string]: unknown;
|
|
38
|
+
drag?: boolean | "x" | "y" | "z";
|
|
39
|
+
dragConstraints?: DragConstraints;
|
|
40
|
+
dragElastic?: number | boolean;
|
|
41
|
+
dragMomentum?: boolean;
|
|
42
|
+
dragSnapToOrigin?: boolean;
|
|
43
|
+
dragTransition?: Transition;
|
|
44
|
+
whileDrag?: Record<string, unknown> | string;
|
|
45
|
+
onDragStart?: (event: PointerEvent, info: DragInfo) => void;
|
|
46
|
+
onDrag?: (event: PointerEvent, info: DragInfo) => void;
|
|
47
|
+
onDragEnd?: (event: PointerEvent, info: DragInfo) => void;
|
|
11
48
|
onInstanceUpdate?: ReactThreeFiber.ThreeElements["object3D"]["onUpdate"];
|
|
12
49
|
onUpdate?: (values: Record<string, unknown>, animationVariant?: string) => void;
|
|
13
50
|
onAnimationStart?: (values: Record<string, unknown>, animationVariant?: string) => void;
|
|
@@ -90,5 +127,22 @@ interface AnimatePresenceProps {
|
|
|
90
127
|
}
|
|
91
128
|
declare function AnimatePresence({ children, mode, onExitComplete, custom, }: AnimatePresenceProps): react.FunctionComponentElement<react.ProviderProps<PresenceContextValue | null>>[];
|
|
92
129
|
|
|
93
|
-
|
|
94
|
-
|
|
130
|
+
interface CarouselProps {
|
|
131
|
+
items: ReactNode[];
|
|
132
|
+
itemWidth?: number;
|
|
133
|
+
gap?: number;
|
|
134
|
+
defaultValue?: number;
|
|
135
|
+
transition?: Transition;
|
|
136
|
+
onSwitch?: (index: number) => void;
|
|
137
|
+
onDragStart?: () => void;
|
|
138
|
+
onDrag?: (info: DragInfo) => void;
|
|
139
|
+
onDragEnd?: (info: DragInfo) => void;
|
|
140
|
+
renderThreshold?: number;
|
|
141
|
+
dragThreshold?: number;
|
|
142
|
+
flickVelocity?: number;
|
|
143
|
+
}
|
|
144
|
+
declare const Carousel: ({ items, itemWidth, gap, defaultValue, transition, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold, flickVelocity, }: CarouselProps) => react_jsx_runtime.JSX.Element;
|
|
145
|
+
declare const _default: typeof Carousel;
|
|
146
|
+
|
|
147
|
+
export { AnimatePresence, _default as Carousel, MotionCamera, motion, usePresence };
|
|
148
|
+
export type { AcceptMotionValues, CarouselProps, DragConstraints, DragInfo, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
|
package/package.json
CHANGED