r3f-motion 1.0.5 → 1.0.7
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 +630 -5
- package/dist/es/components/Carousel/index.mjs +224 -0
- package/dist/es/index.mjs +1 -0
- package/dist/es/render/gestures/use-drag.mjs +389 -0
- package/dist/es/render/motion.mjs +8 -1
- package/dist/index.d.ts +67 -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
|
/**
|
|
@@ -249,6 +269,389 @@ function useTap(isStatic, props, options) {
|
|
|
249
269
|
};
|
|
250
270
|
}
|
|
251
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
|
+
|
|
252
655
|
function createAnimationState() {
|
|
253
656
|
return {
|
|
254
657
|
hasStarted: false,
|
|
@@ -533,14 +936,20 @@ function custom(Component) {
|
|
|
533
936
|
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
534
937
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
535
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
|
+
}, []);
|
|
536
943
|
const gestureProps = {
|
|
944
|
+
instanceRef,
|
|
537
945
|
captureInstanceState,
|
|
538
946
|
buildTargetFromState,
|
|
539
947
|
animateToTarget,
|
|
540
948
|
resolveVariant,
|
|
541
949
|
transition,
|
|
950
|
+
stopAnimation,
|
|
542
951
|
};
|
|
543
|
-
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));
|
|
544
953
|
const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
|
|
545
954
|
const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
|
|
546
955
|
const getNextChildIndex = react.useCallback(() => {
|
|
@@ -581,11 +990,11 @@ const MotionCamera = (props) => {
|
|
|
581
990
|
const aspect = size.width / size.height;
|
|
582
991
|
let cam;
|
|
583
992
|
if (type === "perspective") {
|
|
584
|
-
cam = new
|
|
993
|
+
cam = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
|
585
994
|
}
|
|
586
995
|
else {
|
|
587
996
|
const frustumSize = 10;
|
|
588
|
-
cam = new
|
|
997
|
+
cam = new THREE.OrthographicCamera((-frustumSize * aspect) / 2, (frustumSize * aspect) / 2, frustumSize / 2, -frustumSize / 2, near, far);
|
|
589
998
|
}
|
|
590
999
|
cam.updateProjectionMatrix();
|
|
591
1000
|
return cam;
|
|
@@ -710,7 +1119,223 @@ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
|
|
|
710
1119
|
});
|
|
711
1120
|
}
|
|
712
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
|
+
let isDragging = false;
|
|
1138
|
+
const CarouselSlotContext = react.createContext(null);
|
|
1139
|
+
const useCarouselSlot = () => {
|
|
1140
|
+
const ctx = react.useContext(CarouselSlotContext);
|
|
1141
|
+
if (!ctx)
|
|
1142
|
+
throw new Error('useCarouselSlot must be used inside <Carousel>');
|
|
1143
|
+
return ctx;
|
|
1144
|
+
};
|
|
1145
|
+
const CarouselSlot = react.memo(({ item, itemWidth, slotRef, visible }) => {
|
|
1146
|
+
const contentRef = react.useRef(null);
|
|
1147
|
+
const coverRef = react.useRef(null);
|
|
1148
|
+
react.useLayoutEffect(() => {
|
|
1149
|
+
const content = contentRef.current;
|
|
1150
|
+
const cover = coverRef.current;
|
|
1151
|
+
if (!content || !cover)
|
|
1152
|
+
return;
|
|
1153
|
+
const box = new THREE.Box3().setFromObject(content);
|
|
1154
|
+
if (box.isEmpty())
|
|
1155
|
+
return;
|
|
1156
|
+
const w = Math.max(box.max.x - box.min.x, itemWidth);
|
|
1157
|
+
const h = box.max.y - box.min.y;
|
|
1158
|
+
cover.geometry.dispose();
|
|
1159
|
+
cover.geometry = new THREE.PlaneGeometry(w, h);
|
|
1160
|
+
cover.position.set((box.max.x + box.min.x) / 2, (box.max.y + box.min.y) / 2, box.max.z + 0.001);
|
|
1161
|
+
}, [itemWidth]);
|
|
1162
|
+
// Dispose the generated geometry on unmount — useLayoutEffect's replacement
|
|
1163
|
+
// logic only disposes the *previous* geometry, not the final one.
|
|
1164
|
+
react.useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
|
|
1165
|
+
// No onClick on the cover — the carousel's document-level capture listener
|
|
1166
|
+
// handles click suppression. The cover stays as a drag hit-target so swiping
|
|
1167
|
+
// works on slot regions where user content has gaps.
|
|
1168
|
+
return (jsxRuntime.jsxs("group", { ref: slotRef, children: [jsxRuntime.jsx("group", { ref: contentRef, children: visible ? item : null }), jsxRuntime.jsxs("mesh", { ref: coverRef, children: [jsxRuntime.jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
|
|
1169
|
+
});
|
|
1170
|
+
const Carousel = (_a) => {
|
|
1171
|
+
var { items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY } = _a, props = tslib.__rest(_a, ["items", "itemWidth", "gap", "defaultValue", "transition", "onSwitch", "onDragStart", "onDrag", "onDragEnd", "renderThreshold", "dragThreshold", "flickVelocity"]);
|
|
1172
|
+
const slideWidth = itemWidth + gap;
|
|
1173
|
+
const count = items.length;
|
|
1174
|
+
// Render twice the items and center on the first item of the second copy.
|
|
1175
|
+
// The first copy lives to the left, so wrap-points sit at ±N*slideWidth
|
|
1176
|
+
// from camera center — well off-screen — instead of ±N*slideWidth/2
|
|
1177
|
+
// where they'd pop in and out at the visible edges.
|
|
1178
|
+
const loopedItems = react.useMemo(() => [...items, ...items], [items]);
|
|
1179
|
+
const slotCount = loopedItems.length;
|
|
1180
|
+
const period = slotCount * slideWidth;
|
|
1181
|
+
const startSlot = count + defaultValue;
|
|
1182
|
+
const groupRef = react.useRef(null);
|
|
1183
|
+
const itemRefs = react.useRef([]);
|
|
1184
|
+
// Slot mount/unmount state — mirrors `ref.visible` but propagates through
|
|
1185
|
+
// React so children that rely on lifecycle (e.g. Drei's <Html>, which
|
|
1186
|
+
// doesn't reliably react to mid-frame `visible` mutations) update cleanly.
|
|
1187
|
+
// Only flips when a slot crosses the threshold, so re-renders are limited
|
|
1188
|
+
// to ~2-4 per swipe instead of every frame.
|
|
1189
|
+
const [visibleMask, setVisibleMask] = react.useState(() => {
|
|
1190
|
+
if (renderThreshold === undefined)
|
|
1191
|
+
return new Array(slotCount).fill(true);
|
|
1192
|
+
const mask = new Array(slotCount).fill(false);
|
|
1193
|
+
for (let i = 0; i < slotCount; i++) {
|
|
1194
|
+
mask[i] = Math.abs(i - startSlot) <= renderThreshold + 0.5;
|
|
1195
|
+
}
|
|
1196
|
+
return mask;
|
|
1197
|
+
});
|
|
1198
|
+
const visibleMaskRef = react.useRef(visibleMask);
|
|
1199
|
+
visibleMaskRef.current = visibleMask;
|
|
1200
|
+
// Single source of truth: a fractional slot index that grows up/down
|
|
1201
|
+
// infinitely as the user navigates. group.position.x = -currIndex * slideWidth
|
|
1202
|
+
// at rest. Snap animations animate currIndex toward integer targets;
|
|
1203
|
+
// useFrame applies it to the group every frame (when not dragging).
|
|
1204
|
+
const currIndex = react$1.useMotionValue(startSlot);
|
|
1205
|
+
const isDraggingRef = react.useRef(false);
|
|
1206
|
+
const dragStartIndexRef = react.useRef(startSlot);
|
|
1207
|
+
const snapAnimRef = react.useRef(null);
|
|
1208
|
+
// performance.now() of the most recent frame in which the carousel was
|
|
1209
|
+
// moving (drag in progress or off-snap). Click handler uses this to
|
|
1210
|
+
// suppress clicks during/just-after motion.
|
|
1211
|
+
const lastMotionAtRef = react.useRef(0);
|
|
1212
|
+
// Wrapped physical slot index (0..slotCount-1) — drives per-slot context so
|
|
1213
|
+
// children can read isActive/distance without the consumer having to lift
|
|
1214
|
+
// state. Updates the moment a snap commits, not while dragging.
|
|
1215
|
+
const [currentSlot, setCurrentSlot] = react.useState(() => mod(startSlot, slotCount));
|
|
1216
|
+
const initial = react.useMemo(() => ({ x: -startSlot * slideWidth }),
|
|
1217
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1218
|
+
[]);
|
|
1219
|
+
const goTo = react.useCallback((slot) => {
|
|
1220
|
+
var _a;
|
|
1221
|
+
onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
|
|
1222
|
+
setCurrentSlot(mod(slot, slotCount));
|
|
1223
|
+
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
1224
|
+
snapAnimRef.current = react$1.animate(currIndex, slot, transition);
|
|
1225
|
+
}, [count, slotCount, transition, onSwitch, currIndex]);
|
|
1226
|
+
react.useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
|
|
1227
|
+
// Document-level capture click listener — fires before R3F's listener (and
|
|
1228
|
+
// any descendant DOM handlers) anywhere in the tree. While the carousel is
|
|
1229
|
+
// moving (or just settled), swallow the event so it never reaches user
|
|
1230
|
+
// content (R3F meshes or HTML portaled by Drei). No-op outside the cooldown
|
|
1231
|
+
// window, so true taps propagate normally.
|
|
1232
|
+
react.useEffect(() => {
|
|
1233
|
+
const onClickCapture = (e) => {
|
|
1234
|
+
if (isDragging) {
|
|
1235
|
+
e.stopImmediatePropagation();
|
|
1236
|
+
e.stopPropagation();
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
document.addEventListener('click', onClickCapture, true);
|
|
1240
|
+
return () => document.removeEventListener('click', onClickCapture, true);
|
|
1241
|
+
}, []);
|
|
1242
|
+
// Drag start: stop any in-flight snap and capture the slot we started on
|
|
1243
|
+
// so an under-threshold release can elastic back to it.
|
|
1244
|
+
const handleDragStart = react.useCallback(() => {
|
|
1245
|
+
var _a;
|
|
1246
|
+
isDragging = false;
|
|
1247
|
+
isDraggingRef.current = true;
|
|
1248
|
+
lastMotionAtRef.current = performance.now();
|
|
1249
|
+
dragStartIndexRef.current = Math.round(currIndex.get());
|
|
1250
|
+
(_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
|
|
1251
|
+
snapAnimRef.current = null;
|
|
1252
|
+
onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart();
|
|
1253
|
+
}, [currIndex, onDragStart]);
|
|
1254
|
+
const handleDragEnd = react.useCallback((_, info) => {
|
|
1255
|
+
isDraggingRef.current = false;
|
|
1256
|
+
const group = groupRef.current;
|
|
1257
|
+
if (!group)
|
|
1258
|
+
return;
|
|
1259
|
+
// Resync the motion value to where use-drag actually locked the position
|
|
1260
|
+
// — the next useFrame will use this same value, so there's no visible
|
|
1261
|
+
// jump between the dragged position and the start of the snap tween.
|
|
1262
|
+
const releasedX = group.position.x;
|
|
1263
|
+
const releasedIndex = -releasedX / slideWidth;
|
|
1264
|
+
currIndex.jump(releasedIndex);
|
|
1265
|
+
const startIndex = dragStartIndexRef.current;
|
|
1266
|
+
const offsetIndex = releasedIndex - startIndex;
|
|
1267
|
+
const overThreshold = Math.abs(offsetIndex) > dragThreshold;
|
|
1268
|
+
const overVelocity = Math.abs(info.velocity.x) > flickVelocity;
|
|
1269
|
+
if (!overThreshold && !overVelocity) {
|
|
1270
|
+
goTo(startIndex);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
|
|
1274
|
+
const targetSlot = Math.round(-projectedX / slideWidth);
|
|
1275
|
+
goTo(targetSlot);
|
|
1276
|
+
onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(info);
|
|
1277
|
+
}, [slideWidth, goTo, currIndex, dragThreshold, flickVelocity, onDragEnd]);
|
|
1278
|
+
// Drive position from currIndex unless the user is dragging — during drag
|
|
1279
|
+
// use-drag writes to group.position.x directly (and dragMomentum=false
|
|
1280
|
+
// ensures it stops writing the moment the pointer lifts, so there's no
|
|
1281
|
+
// race with the snap animation).
|
|
1282
|
+
fiber.useFrame(() => {
|
|
1283
|
+
const group = groupRef.current;
|
|
1284
|
+
if (!group)
|
|
1285
|
+
return;
|
|
1286
|
+
if (!isDraggingRef.current) {
|
|
1287
|
+
group.position.x = -currIndex.get() * slideWidth;
|
|
1288
|
+
}
|
|
1289
|
+
const groupX = group.position.x;
|
|
1290
|
+
const snapDelta = Math.abs(groupX - Math.round(groupX / slideWidth) * slideWidth);
|
|
1291
|
+
if (isDraggingRef.current || snapDelta > 0.001) {
|
|
1292
|
+
lastMotionAtRef.current = performance.now();
|
|
1293
|
+
isDragging = snapDelta > 0.001;
|
|
1294
|
+
}
|
|
1295
|
+
let nextMask = null;
|
|
1296
|
+
for (let i = 0; i < slotCount; i++) {
|
|
1297
|
+
const ref = itemRefs.current[i];
|
|
1298
|
+
if (!ref)
|
|
1299
|
+
continue;
|
|
1300
|
+
const k = Math.round((-groupX - i * slideWidth) / period);
|
|
1301
|
+
ref.position.x = i * slideWidth + k * period;
|
|
1302
|
+
// +0.5 margin so the leading-edge slot fades in the moment the trailing
|
|
1303
|
+
// one fades out — keeps the loop seamless mid-drag instead of waiting
|
|
1304
|
+
// for snap to settle on an integer slot.
|
|
1305
|
+
const slotDist = (groupX + ref.position.x) / slideWidth;
|
|
1306
|
+
const shouldBeVisible = renderThreshold ? Math.abs(slotDist) <= renderThreshold + 0.5 : true;
|
|
1307
|
+
ref.visible = shouldBeVisible;
|
|
1308
|
+
if (visibleMaskRef.current[i] !== shouldBeVisible) {
|
|
1309
|
+
if (!nextMask)
|
|
1310
|
+
nextMask = visibleMaskRef.current.slice();
|
|
1311
|
+
nextMask[i] = shouldBeVisible;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (nextMask) {
|
|
1315
|
+
visibleMaskRef.current = nextMask;
|
|
1316
|
+
setVisibleMask(nextMask);
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
return (jsxRuntime.jsx(motion.group, Object.assign({ ref: groupRef, drag: "x", dragMomentum: false, initial: initial }, props, { onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => {
|
|
1320
|
+
var _a;
|
|
1321
|
+
const raw = i - currentSlot;
|
|
1322
|
+
const halfWrap = (((raw + slotCount / 2) % slotCount) + slotCount) % slotCount;
|
|
1323
|
+
const distance = Math.abs(halfWrap - slotCount / 2);
|
|
1324
|
+
return (jsxRuntime.jsx(CarouselSlotContext.Provider, { value: {
|
|
1325
|
+
slotIndex: i,
|
|
1326
|
+
itemIndex: i % count,
|
|
1327
|
+
distance,
|
|
1328
|
+
isActive: distance === 0,
|
|
1329
|
+
isNearby: distance <= 1,
|
|
1330
|
+
currIndex,
|
|
1331
|
+
}, children: jsxRuntime.jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, visible: (_a = visibleMask[i]) !== null && _a !== void 0 ? _a : false }) }, `carouselItem-${i}`));
|
|
1332
|
+
}) })));
|
|
1333
|
+
};
|
|
1334
|
+
var index = react.memo(Carousel);
|
|
1335
|
+
|
|
713
1336
|
exports.AnimatePresence = AnimatePresence;
|
|
1337
|
+
exports.Carousel = index;
|
|
714
1338
|
exports.MotionCamera = MotionCamera;
|
|
715
1339
|
exports.motion = motion;
|
|
1340
|
+
exports.useCarouselSlot = useCarouselSlot;
|
|
716
1341
|
exports.usePresence = usePresence;
|