r3f-motion 1.0.3 → 1.0.5
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 +210 -40
- package/dist/es/components/AnimatePresence/PresenceContext.mjs +22 -0
- package/dist/es/components/AnimatePresence/index.mjs +119 -0
- package/dist/es/index.mjs +2 -0
- package/dist/es/render/motion.mjs +53 -20
- package/dist/es/render/use-render.mjs +25 -21
- package/dist/index.d.ts +41 -2
- package/package.json +2 -1
package/dist/cjs/index.js
CHANGED
|
@@ -8,18 +8,37 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
8
8
|
var fiber = require('@react-three/fiber');
|
|
9
9
|
var three = require('three');
|
|
10
10
|
|
|
11
|
+
const PresenceContext = react.createContext(null);
|
|
12
|
+
/**
|
|
13
|
+
* Returns presence information for the current component.
|
|
14
|
+
*
|
|
15
|
+
* - `isPresent`: Whether the component is currently present in the tree.
|
|
16
|
+
* When `false`, the component is exiting and should play its exit animation.
|
|
17
|
+
* - `safeToRemove`: Call this function when the exit animation is complete
|
|
18
|
+
* to signal that the component can be unmounted.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` if the component is not wrapped in `AnimatePresence`.
|
|
21
|
+
*/
|
|
22
|
+
function usePresence() {
|
|
23
|
+
const context = react.useContext(PresenceContext);
|
|
24
|
+
if (!context)
|
|
25
|
+
return null;
|
|
26
|
+
return [context.isPresent, context.safeToRemove];
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
const useRender = (Component, props, forwardedRef, instanceRef, initialValues) => {
|
|
12
30
|
const initialValuesAppliedRef = react.useRef(false);
|
|
13
31
|
/**
|
|
14
32
|
* Create a callback ref that captures the Three.js instance
|
|
15
33
|
*/
|
|
16
34
|
const callbackRef = react.useCallback((instance) => {
|
|
35
|
+
var _a;
|
|
17
36
|
if (!instance)
|
|
18
37
|
return;
|
|
19
38
|
// Apply initial values immediately to prevent FOUC - but only once
|
|
20
39
|
if (initialValues && !initialValuesAppliedRef.current) {
|
|
21
40
|
initialValuesAppliedRef.current = true;
|
|
22
|
-
//
|
|
41
|
+
// Transform property mapping
|
|
23
42
|
const propertyMap = {
|
|
24
43
|
x: (val) => instance.position && (instance.position.x = val),
|
|
25
44
|
y: (val) => instance.position && (instance.position.y = val),
|
|
@@ -32,32 +51,35 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
|
|
|
32
51
|
scaleX: (val) => instance.scale && (instance.scale.x = val),
|
|
33
52
|
scaleY: (val) => instance.scale && (instance.scale.y = val),
|
|
34
53
|
scaleZ: (val) => instance.scale && (instance.scale.z = val),
|
|
35
|
-
color: (val) => {
|
|
36
|
-
const color = instance.color;
|
|
37
|
-
if (color && color.set) {
|
|
38
|
-
color.set(val);
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
opacity: (val) => instance.opacity !== undefined &&
|
|
42
|
-
(instance.opacity = val),
|
|
43
|
-
emissive: (val) => {
|
|
44
|
-
const emissive = instance.emissive;
|
|
45
|
-
if (emissive && emissive.set) {
|
|
46
|
-
emissive.set(val);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
emissiveIntensity: (val) => instance.emissiveIntensity !== undefined &&
|
|
50
|
-
(instance.emissiveIntensity = val),
|
|
51
|
-
roughness: (val) => instance.roughness !== undefined &&
|
|
52
|
-
(instance.roughness = val),
|
|
53
|
-
metalness: (val) => instance.metalness !== undefined &&
|
|
54
|
-
(instance.metalness = val),
|
|
55
54
|
};
|
|
55
|
+
// Color-type properties that need .set()
|
|
56
|
+
const colorKeys = new Set([
|
|
57
|
+
"color",
|
|
58
|
+
"emissive",
|
|
59
|
+
"specular",
|
|
60
|
+
"sheenColor",
|
|
61
|
+
"attenuationColor",
|
|
62
|
+
]);
|
|
56
63
|
for (const key in initialValues) {
|
|
57
64
|
const setter = propertyMap[key];
|
|
58
65
|
if (setter) {
|
|
59
66
|
setter(initialValues[key]);
|
|
60
67
|
}
|
|
68
|
+
else if (colorKeys.has(key)) {
|
|
69
|
+
const colorProp = instance[key];
|
|
70
|
+
if (colorProp && colorProp.set) {
|
|
71
|
+
colorProp.set(initialValues[key]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (instance.uniforms &&
|
|
75
|
+
instance.uniforms[key] &&
|
|
76
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
77
|
+
// Set ShaderMaterial uniform initial value
|
|
78
|
+
instance.uniforms[key].value = initialValues[key];
|
|
79
|
+
}
|
|
80
|
+
else if (key in instance && typeof instance[key] === "number") {
|
|
81
|
+
instance[key] = initialValues[key];
|
|
82
|
+
}
|
|
61
83
|
}
|
|
62
84
|
}
|
|
63
85
|
// Store instance in the ref so animations can access it
|
|
@@ -286,7 +308,8 @@ function custom(Component) {
|
|
|
286
308
|
const childIndex = react.useMemo(() => { var _a, _b; return (_b = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.getNextChildIndex) === null || _a === void 0 ? void 0 : _a.call(parentContext)) !== null && _b !== void 0 ? _b : 0; },
|
|
287
309
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
288
310
|
[]);
|
|
289
|
-
const
|
|
311
|
+
const presenceContext = react.useContext(PresenceContext);
|
|
312
|
+
const _a = props, { initial: initialProp, animate: animateProp, exit: exitProp, transition, variants, custom, inherit = true, children, onAnimationUpdate, onAnimationStart, onAnimationComplete } = _a, restProps = tslib.__rest(_a, ["initial", "animate", "exit", "transition", "variants", "custom", "inherit", "children", "onAnimationUpdate", "onAnimationStart", "onAnimationComplete"]);
|
|
290
313
|
const initial = initialProp !== undefined
|
|
291
314
|
? initialProp
|
|
292
315
|
: inherit && (parentContext === null || parentContext === void 0 ? void 0 : parentContext.initial);
|
|
@@ -422,7 +445,16 @@ function custom(Component) {
|
|
|
422
445
|
const tempColor = new ColorConstructor(value);
|
|
423
446
|
["r", "g", "b"].forEach((channel) => createAnimation(target, { [channel]: tempColor[channel] }, opts, key));
|
|
424
447
|
};
|
|
448
|
+
// Color-type properties that need RGB channel animation
|
|
449
|
+
const colorKeys = new Set([
|
|
450
|
+
"color",
|
|
451
|
+
"emissive",
|
|
452
|
+
"specular",
|
|
453
|
+
"sheenColor",
|
|
454
|
+
"attenuationColor",
|
|
455
|
+
]);
|
|
425
456
|
Object.entries(targetValues).forEach(([key, value]) => {
|
|
457
|
+
var _a;
|
|
426
458
|
const opts = getPropertyOpts(key);
|
|
427
459
|
const mapping = transformMap[key];
|
|
428
460
|
if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
|
|
@@ -433,26 +465,19 @@ function custom(Component) {
|
|
|
433
465
|
createAnimation(mapping.target, { [mapping.prop]: value }, opts, key);
|
|
434
466
|
}
|
|
435
467
|
}
|
|
436
|
-
else if (key
|
|
437
|
-
animateColor(instance
|
|
438
|
-
}
|
|
439
|
-
else if (key === "emissive" && instance.emissive) {
|
|
440
|
-
animateColor(instance.emissive, value, opts, key);
|
|
468
|
+
else if (colorKeys.has(key) && instance[key]) {
|
|
469
|
+
animateColor(instance[key], value, opts, key);
|
|
441
470
|
}
|
|
442
|
-
else if (
|
|
443
|
-
|
|
471
|
+
else if (instance.uniforms &&
|
|
472
|
+
instance.uniforms[key] &&
|
|
473
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
474
|
+
// Animate ShaderMaterial uniforms (uniforms[key].value)
|
|
475
|
+
// Note: consumers must useMemo their uniforms prop to prevent
|
|
476
|
+
// R3F from overwriting animated values on re-render
|
|
477
|
+
createAnimation(instance.uniforms[key], { value: value }, opts, key);
|
|
444
478
|
}
|
|
445
|
-
else if (key === "
|
|
446
|
-
instance
|
|
447
|
-
createAnimation(instance, { emissiveIntensity: value }, opts, key);
|
|
448
|
-
}
|
|
449
|
-
else if (key === "roughness" &&
|
|
450
|
-
instance.roughness !== undefined) {
|
|
451
|
-
createAnimation(instance, { roughness: value }, opts, key);
|
|
452
|
-
}
|
|
453
|
-
else if (key === "metalness" &&
|
|
454
|
-
instance.metalness !== undefined) {
|
|
455
|
-
createAnimation(instance, { metalness: value }, opts, key);
|
|
479
|
+
else if (key in instance && typeof instance[key] === "number") {
|
|
480
|
+
createAnimation(instance, { [key]: value }, opts, key);
|
|
456
481
|
}
|
|
457
482
|
});
|
|
458
483
|
animationRef.current = {
|
|
@@ -479,6 +504,35 @@ function custom(Component) {
|
|
|
479
504
|
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
480
505
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
481
506
|
}, [animate]);
|
|
507
|
+
// Handle exit animation when presence context signals removal
|
|
508
|
+
react.useEffect(() => {
|
|
509
|
+
var _a;
|
|
510
|
+
if (!presenceContext || presenceContext.isPresent)
|
|
511
|
+
return;
|
|
512
|
+
if (!instanceRef.current || !exitProp) {
|
|
513
|
+
// No exit animation defined, safe to remove immediately
|
|
514
|
+
presenceContext.safeToRemove();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const resolved = typeof exitProp === "string" ? resolveVariant(exitProp) : exitProp;
|
|
518
|
+
if (!resolved) {
|
|
519
|
+
presenceContext.safeToRemove();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const resolvedObj = resolved;
|
|
523
|
+
const { transition: exitTransition } = resolvedObj, exitValues = tslib.__rest(resolvedObj, ["transition"]);
|
|
524
|
+
const effectiveExitTransition = exitTransition || transition;
|
|
525
|
+
// Store safeToRemove in a ref-like closure so onAnimationComplete can call it
|
|
526
|
+
const originalComplete = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAnimationComplete;
|
|
527
|
+
const safeToRemove = presenceContext.safeToRemove;
|
|
528
|
+
callbacksRef.current = Object.assign(Object.assign({}, callbacksRef.current), { onAnimationComplete: (variant) => {
|
|
529
|
+
originalComplete === null || originalComplete === void 0 ? void 0 : originalComplete(variant);
|
|
530
|
+
safeToRemove();
|
|
531
|
+
} });
|
|
532
|
+
animateToTarget(exitValues, effectiveExitTransition, true);
|
|
533
|
+
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
534
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
535
|
+
}, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
|
|
482
536
|
const gestureProps = {
|
|
483
537
|
captureInstanceState,
|
|
484
538
|
buildTargetFromState,
|
|
@@ -542,5 +596,121 @@ const MotionCamera = (props) => {
|
|
|
542
596
|
return jsxRuntime.jsx(motion.primitive, Object.assign({ object: camera }, restProps));
|
|
543
597
|
};
|
|
544
598
|
|
|
599
|
+
function getChildKey(child) {
|
|
600
|
+
var _a;
|
|
601
|
+
return (_a = child.key) !== null && _a !== void 0 ? _a : "";
|
|
602
|
+
}
|
|
603
|
+
function getValidChildren(children) {
|
|
604
|
+
const result = [];
|
|
605
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
606
|
+
for (const child of childArray) {
|
|
607
|
+
if (react.isValidElement(child)) {
|
|
608
|
+
result.push(child);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
|
|
614
|
+
const validChildren = getValidChildren(children);
|
|
615
|
+
const [entries, setEntries] = react.useState(() => validChildren.map((child) => ({
|
|
616
|
+
key: getChildKey(child),
|
|
617
|
+
element: child,
|
|
618
|
+
isPresent: true,
|
|
619
|
+
})));
|
|
620
|
+
const exitingCount = react.useRef(0);
|
|
621
|
+
const onExitCompleteRef = react.useRef(onExitComplete);
|
|
622
|
+
onExitCompleteRef.current = onExitComplete;
|
|
623
|
+
// Track whether we're waiting for exits to complete (for mode="wait")
|
|
624
|
+
const [isWaiting, setIsWaiting] = react.useState(false);
|
|
625
|
+
const pendingChildrenRef = react.useRef(null);
|
|
626
|
+
react.useEffect(() => {
|
|
627
|
+
const currentKeys = new Set(validChildren.map(getChildKey));
|
|
628
|
+
const prevKeys = new Set(entries.map((e) => e.key));
|
|
629
|
+
// Find new children to add
|
|
630
|
+
const entering = validChildren.filter((c) => !prevKeys.has(getChildKey(c)));
|
|
631
|
+
// Find children to remove
|
|
632
|
+
const exitingKeys = new Set(entries
|
|
633
|
+
.filter((e) => e.isPresent && !currentKeys.has(e.key))
|
|
634
|
+
.map((e) => e.key));
|
|
635
|
+
if (exitingKeys.size === 0 && entering.length === 0) {
|
|
636
|
+
// Just update existing children elements
|
|
637
|
+
setEntries((prev) => prev.map((entry) => {
|
|
638
|
+
const updated = validChildren.find((c) => getChildKey(c) === entry.key);
|
|
639
|
+
return updated ? Object.assign(Object.assign({}, entry), { element: updated }) : entry;
|
|
640
|
+
}));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (mode === "wait" && exitingKeys.size > 0 && entering.length > 0) {
|
|
644
|
+
// Store pending children, mark exits, wait for them to complete
|
|
645
|
+
pendingChildrenRef.current = validChildren;
|
|
646
|
+
setIsWaiting(true);
|
|
647
|
+
exitingCount.current = exitingKeys.size;
|
|
648
|
+
setEntries((prev) => prev.map((entry) => exitingKeys.has(entry.key)
|
|
649
|
+
? Object.assign(Object.assign({}, entry), { isPresent: false }) : entry));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
exitingCount.current = exitingKeys.size;
|
|
653
|
+
setEntries((prev) => {
|
|
654
|
+
// Update existing, mark exiting
|
|
655
|
+
const updated = prev.map((entry) => {
|
|
656
|
+
if (exitingKeys.has(entry.key)) {
|
|
657
|
+
return Object.assign(Object.assign({}, entry), { isPresent: false });
|
|
658
|
+
}
|
|
659
|
+
const updatedChild = validChildren.find((c) => getChildKey(c) === entry.key);
|
|
660
|
+
return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
|
|
661
|
+
});
|
|
662
|
+
// Add entering children
|
|
663
|
+
const newEntries = entering.map((child) => ({
|
|
664
|
+
key: getChildKey(child),
|
|
665
|
+
element: child,
|
|
666
|
+
isPresent: true,
|
|
667
|
+
}));
|
|
668
|
+
return [...updated, ...newEntries];
|
|
669
|
+
});
|
|
670
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
671
|
+
}, [children]);
|
|
672
|
+
const handleSafeToRemove = (key) => {
|
|
673
|
+
var _a;
|
|
674
|
+
setEntries((prev) => prev.filter((entry) => entry.key !== key));
|
|
675
|
+
exitingCount.current--;
|
|
676
|
+
if (exitingCount.current <= 0) {
|
|
677
|
+
(_a = onExitCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onExitCompleteRef);
|
|
678
|
+
// If in wait mode and we have pending children, add them now
|
|
679
|
+
if (isWaiting && pendingChildrenRef.current) {
|
|
680
|
+
const pending = pendingChildrenRef.current;
|
|
681
|
+
pendingChildrenRef.current = null;
|
|
682
|
+
setIsWaiting(false);
|
|
683
|
+
setEntries((prev) => {
|
|
684
|
+
const remaining = prev; // exiting ones already filtered out above
|
|
685
|
+
const currentKeys = new Set(remaining.map((e) => e.key));
|
|
686
|
+
const newEntries = pending
|
|
687
|
+
.filter((c) => !currentKeys.has(getChildKey(c)))
|
|
688
|
+
.map((child) => ({
|
|
689
|
+
key: getChildKey(child),
|
|
690
|
+
element: child,
|
|
691
|
+
isPresent: true,
|
|
692
|
+
}));
|
|
693
|
+
// Update existing entries with new elements
|
|
694
|
+
const updated = remaining.map((entry) => {
|
|
695
|
+
const updatedChild = pending.find((c) => getChildKey(c) === entry.key);
|
|
696
|
+
return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
|
|
697
|
+
});
|
|
698
|
+
return [...updated, ...newEntries];
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
return entries.map((entry) => {
|
|
704
|
+
const contextValue = {
|
|
705
|
+
isPresent: entry.isPresent,
|
|
706
|
+
safeToRemove: () => handleSafeToRemove(entry.key),
|
|
707
|
+
custom,
|
|
708
|
+
};
|
|
709
|
+
return react.createElement(PresenceContext.Provider, { key: entry.key, value: contextValue }, react.cloneElement(entry.element));
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
exports.AnimatePresence = AnimatePresence;
|
|
545
714
|
exports.MotionCamera = MotionCamera;
|
|
546
715
|
exports.motion = motion;
|
|
716
|
+
exports.usePresence = usePresence;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
const PresenceContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Returns presence information for the current component.
|
|
7
|
+
*
|
|
8
|
+
* - `isPresent`: Whether the component is currently present in the tree.
|
|
9
|
+
* When `false`, the component is exiting and should play its exit animation.
|
|
10
|
+
* - `safeToRemove`: Call this function when the exit animation is complete
|
|
11
|
+
* to signal that the component can be unmounted.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` if the component is not wrapped in `AnimatePresence`.
|
|
14
|
+
*/
|
|
15
|
+
function usePresence() {
|
|
16
|
+
const context = useContext(PresenceContext);
|
|
17
|
+
if (!context)
|
|
18
|
+
return null;
|
|
19
|
+
return [context.isPresent, context.safeToRemove];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { PresenceContext, usePresence };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useRef, useEffect, createElement, cloneElement, isValidElement } from 'react';
|
|
3
|
+
import { PresenceContext } from './PresenceContext.mjs';
|
|
4
|
+
|
|
5
|
+
function getChildKey(child) {
|
|
6
|
+
var _a;
|
|
7
|
+
return (_a = child.key) !== null && _a !== void 0 ? _a : "";
|
|
8
|
+
}
|
|
9
|
+
function getValidChildren(children) {
|
|
10
|
+
const result = [];
|
|
11
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
12
|
+
for (const child of childArray) {
|
|
13
|
+
if (isValidElement(child)) {
|
|
14
|
+
result.push(child);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
|
|
20
|
+
const validChildren = getValidChildren(children);
|
|
21
|
+
const [entries, setEntries] = useState(() => validChildren.map((child) => ({
|
|
22
|
+
key: getChildKey(child),
|
|
23
|
+
element: child,
|
|
24
|
+
isPresent: true,
|
|
25
|
+
})));
|
|
26
|
+
const exitingCount = useRef(0);
|
|
27
|
+
const onExitCompleteRef = useRef(onExitComplete);
|
|
28
|
+
onExitCompleteRef.current = onExitComplete;
|
|
29
|
+
// Track whether we're waiting for exits to complete (for mode="wait")
|
|
30
|
+
const [isWaiting, setIsWaiting] = useState(false);
|
|
31
|
+
const pendingChildrenRef = useRef(null);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const currentKeys = new Set(validChildren.map(getChildKey));
|
|
34
|
+
const prevKeys = new Set(entries.map((e) => e.key));
|
|
35
|
+
// Find new children to add
|
|
36
|
+
const entering = validChildren.filter((c) => !prevKeys.has(getChildKey(c)));
|
|
37
|
+
// Find children to remove
|
|
38
|
+
const exitingKeys = new Set(entries
|
|
39
|
+
.filter((e) => e.isPresent && !currentKeys.has(e.key))
|
|
40
|
+
.map((e) => e.key));
|
|
41
|
+
if (exitingKeys.size === 0 && entering.length === 0) {
|
|
42
|
+
// Just update existing children elements
|
|
43
|
+
setEntries((prev) => prev.map((entry) => {
|
|
44
|
+
const updated = validChildren.find((c) => getChildKey(c) === entry.key);
|
|
45
|
+
return updated ? Object.assign(Object.assign({}, entry), { element: updated }) : entry;
|
|
46
|
+
}));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (mode === "wait" && exitingKeys.size > 0 && entering.length > 0) {
|
|
50
|
+
// Store pending children, mark exits, wait for them to complete
|
|
51
|
+
pendingChildrenRef.current = validChildren;
|
|
52
|
+
setIsWaiting(true);
|
|
53
|
+
exitingCount.current = exitingKeys.size;
|
|
54
|
+
setEntries((prev) => prev.map((entry) => exitingKeys.has(entry.key)
|
|
55
|
+
? Object.assign(Object.assign({}, entry), { isPresent: false }) : entry));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
exitingCount.current = exitingKeys.size;
|
|
59
|
+
setEntries((prev) => {
|
|
60
|
+
// Update existing, mark exiting
|
|
61
|
+
const updated = prev.map((entry) => {
|
|
62
|
+
if (exitingKeys.has(entry.key)) {
|
|
63
|
+
return Object.assign(Object.assign({}, entry), { isPresent: false });
|
|
64
|
+
}
|
|
65
|
+
const updatedChild = validChildren.find((c) => getChildKey(c) === entry.key);
|
|
66
|
+
return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
|
|
67
|
+
});
|
|
68
|
+
// Add entering children
|
|
69
|
+
const newEntries = entering.map((child) => ({
|
|
70
|
+
key: getChildKey(child),
|
|
71
|
+
element: child,
|
|
72
|
+
isPresent: true,
|
|
73
|
+
}));
|
|
74
|
+
return [...updated, ...newEntries];
|
|
75
|
+
});
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [children]);
|
|
78
|
+
const handleSafeToRemove = (key) => {
|
|
79
|
+
var _a;
|
|
80
|
+
setEntries((prev) => prev.filter((entry) => entry.key !== key));
|
|
81
|
+
exitingCount.current--;
|
|
82
|
+
if (exitingCount.current <= 0) {
|
|
83
|
+
(_a = onExitCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onExitCompleteRef);
|
|
84
|
+
// If in wait mode and we have pending children, add them now
|
|
85
|
+
if (isWaiting && pendingChildrenRef.current) {
|
|
86
|
+
const pending = pendingChildrenRef.current;
|
|
87
|
+
pendingChildrenRef.current = null;
|
|
88
|
+
setIsWaiting(false);
|
|
89
|
+
setEntries((prev) => {
|
|
90
|
+
const remaining = prev; // exiting ones already filtered out above
|
|
91
|
+
const currentKeys = new Set(remaining.map((e) => e.key));
|
|
92
|
+
const newEntries = pending
|
|
93
|
+
.filter((c) => !currentKeys.has(getChildKey(c)))
|
|
94
|
+
.map((child) => ({
|
|
95
|
+
key: getChildKey(child),
|
|
96
|
+
element: child,
|
|
97
|
+
isPresent: true,
|
|
98
|
+
}));
|
|
99
|
+
// Update existing entries with new elements
|
|
100
|
+
const updated = remaining.map((entry) => {
|
|
101
|
+
const updatedChild = pending.find((c) => getChildKey(c) === entry.key);
|
|
102
|
+
return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
|
|
103
|
+
});
|
|
104
|
+
return [...updated, ...newEntries];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
return entries.map((entry) => {
|
|
110
|
+
const contextValue = {
|
|
111
|
+
isPresent: entry.isPresent,
|
|
112
|
+
safeToRemove: () => handleSafeToRemove(entry.key),
|
|
113
|
+
custom,
|
|
114
|
+
};
|
|
115
|
+
return createElement(PresenceContext.Provider, { key: entry.key, value: contextValue }, cloneElement(entry.element));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { AnimatePresence };
|
package/dist/es/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
export { motion } from './render/motion.mjs';
|
|
3
3
|
export { default as MotionCamera } from './components/MotionCamera.mjs';
|
|
4
|
+
export { AnimatePresence } from './components/AnimatePresence/index.mjs';
|
|
5
|
+
export { usePresence } from './components/AnimatePresence/PresenceContext.mjs';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { __rest } from 'tslib';
|
|
3
|
-
import {
|
|
3
|
+
import { forwardRef, useRef, useContext, useMemo, useCallback, useEffect, createElement, memo, createContext } from 'react';
|
|
4
4
|
import { animate } from 'motion';
|
|
5
|
+
import { PresenceContext } from '../components/AnimatePresence/PresenceContext.mjs';
|
|
5
6
|
import { useRender } from './use-render.mjs';
|
|
6
7
|
import { useHover } from './gestures/use-hover.mjs';
|
|
7
8
|
import { useTap } from './gestures/use-tap.mjs';
|
|
@@ -19,7 +20,8 @@ function custom(Component) {
|
|
|
19
20
|
const childIndex = useMemo(() => { var _a, _b; return (_b = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.getNextChildIndex) === null || _a === void 0 ? void 0 : _a.call(parentContext)) !== null && _b !== void 0 ? _b : 0; },
|
|
20
21
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
22
|
[]);
|
|
22
|
-
const
|
|
23
|
+
const presenceContext = useContext(PresenceContext);
|
|
24
|
+
const _a = props, { initial: initialProp, animate: animateProp, exit: exitProp, transition, variants, custom, inherit = true, children, onAnimationUpdate, onAnimationStart, onAnimationComplete } = _a, restProps = __rest(_a, ["initial", "animate", "exit", "transition", "variants", "custom", "inherit", "children", "onAnimationUpdate", "onAnimationStart", "onAnimationComplete"]);
|
|
23
25
|
const initial = initialProp !== undefined
|
|
24
26
|
? initialProp
|
|
25
27
|
: inherit && (parentContext === null || parentContext === void 0 ? void 0 : parentContext.initial);
|
|
@@ -155,7 +157,16 @@ function custom(Component) {
|
|
|
155
157
|
const tempColor = new ColorConstructor(value);
|
|
156
158
|
["r", "g", "b"].forEach((channel) => createAnimation(target, { [channel]: tempColor[channel] }, opts, key));
|
|
157
159
|
};
|
|
160
|
+
// Color-type properties that need RGB channel animation
|
|
161
|
+
const colorKeys = new Set([
|
|
162
|
+
"color",
|
|
163
|
+
"emissive",
|
|
164
|
+
"specular",
|
|
165
|
+
"sheenColor",
|
|
166
|
+
"attenuationColor",
|
|
167
|
+
]);
|
|
158
168
|
Object.entries(targetValues).forEach(([key, value]) => {
|
|
169
|
+
var _a;
|
|
159
170
|
const opts = getPropertyOpts(key);
|
|
160
171
|
const mapping = transformMap[key];
|
|
161
172
|
if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
|
|
@@ -166,26 +177,19 @@ function custom(Component) {
|
|
|
166
177
|
createAnimation(mapping.target, { [mapping.prop]: value }, opts, key);
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
|
-
else if (key
|
|
170
|
-
animateColor(instance
|
|
180
|
+
else if (colorKeys.has(key) && instance[key]) {
|
|
181
|
+
animateColor(instance[key], value, opts, key);
|
|
171
182
|
}
|
|
172
|
-
else if (
|
|
173
|
-
|
|
183
|
+
else if (instance.uniforms &&
|
|
184
|
+
instance.uniforms[key] &&
|
|
185
|
+
typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
|
|
186
|
+
// Animate ShaderMaterial uniforms (uniforms[key].value)
|
|
187
|
+
// Note: consumers must useMemo their uniforms prop to prevent
|
|
188
|
+
// R3F from overwriting animated values on re-render
|
|
189
|
+
createAnimation(instance.uniforms[key], { value: value }, opts, key);
|
|
174
190
|
}
|
|
175
|
-
else if (key
|
|
176
|
-
createAnimation(instance, {
|
|
177
|
-
}
|
|
178
|
-
else if (key === "emissiveIntensity" &&
|
|
179
|
-
instance.emissiveIntensity !== undefined) {
|
|
180
|
-
createAnimation(instance, { emissiveIntensity: value }, opts, key);
|
|
181
|
-
}
|
|
182
|
-
else if (key === "roughness" &&
|
|
183
|
-
instance.roughness !== undefined) {
|
|
184
|
-
createAnimation(instance, { roughness: value }, opts, key);
|
|
185
|
-
}
|
|
186
|
-
else if (key === "metalness" &&
|
|
187
|
-
instance.metalness !== undefined) {
|
|
188
|
-
createAnimation(instance, { metalness: value }, opts, key);
|
|
191
|
+
else if (key in instance && typeof instance[key] === "number") {
|
|
192
|
+
createAnimation(instance, { [key]: value }, opts, key);
|
|
189
193
|
}
|
|
190
194
|
});
|
|
191
195
|
animationRef.current = {
|
|
@@ -212,6 +216,35 @@ function custom(Component) {
|
|
|
212
216
|
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
213
217
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
218
|
}, [animate$1]);
|
|
219
|
+
// Handle exit animation when presence context signals removal
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
var _a;
|
|
222
|
+
if (!presenceContext || presenceContext.isPresent)
|
|
223
|
+
return;
|
|
224
|
+
if (!instanceRef.current || !exitProp) {
|
|
225
|
+
// No exit animation defined, safe to remove immediately
|
|
226
|
+
presenceContext.safeToRemove();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const resolved = typeof exitProp === "string" ? resolveVariant(exitProp) : exitProp;
|
|
230
|
+
if (!resolved) {
|
|
231
|
+
presenceContext.safeToRemove();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const resolvedObj = resolved;
|
|
235
|
+
const { transition: exitTransition } = resolvedObj, exitValues = __rest(resolvedObj, ["transition"]);
|
|
236
|
+
const effectiveExitTransition = exitTransition || transition;
|
|
237
|
+
// Store safeToRemove in a ref-like closure so onAnimationComplete can call it
|
|
238
|
+
const originalComplete = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAnimationComplete;
|
|
239
|
+
const safeToRemove = presenceContext.safeToRemove;
|
|
240
|
+
callbacksRef.current = Object.assign(Object.assign({}, callbacksRef.current), { onAnimationComplete: (variant) => {
|
|
241
|
+
originalComplete === null || originalComplete === void 0 ? void 0 : originalComplete(variant);
|
|
242
|
+
safeToRemove();
|
|
243
|
+
} });
|
|
244
|
+
animateToTarget(exitValues, effectiveExitTransition, true);
|
|
245
|
+
return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
|
|
246
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
247
|
+
}, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
|
|
215
248
|
const gestureProps = {
|
|
216
249
|
captureInstanceState,
|
|
217
250
|
buildTargetFromState,
|
|
@@ -7,12 +7,13 @@ 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
|
|
13
14
|
if (initialValues && !initialValuesAppliedRef.current) {
|
|
14
15
|
initialValuesAppliedRef.current = true;
|
|
15
|
-
//
|
|
16
|
+
// Transform property mapping
|
|
16
17
|
const propertyMap = {
|
|
17
18
|
x: (val) => instance.position && (instance.position.x = val),
|
|
18
19
|
y: (val) => instance.position && (instance.position.y = val),
|
|
@@ -25,32 +26,35 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
|
|
|
25
26
|
scaleX: (val) => instance.scale && (instance.scale.x = val),
|
|
26
27
|
scaleY: (val) => instance.scale && (instance.scale.y = val),
|
|
27
28
|
scaleZ: (val) => instance.scale && (instance.scale.z = val),
|
|
28
|
-
color: (val) => {
|
|
29
|
-
const color = instance.color;
|
|
30
|
-
if (color && color.set) {
|
|
31
|
-
color.set(val);
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
opacity: (val) => instance.opacity !== undefined &&
|
|
35
|
-
(instance.opacity = val),
|
|
36
|
-
emissive: (val) => {
|
|
37
|
-
const emissive = instance.emissive;
|
|
38
|
-
if (emissive && emissive.set) {
|
|
39
|
-
emissive.set(val);
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
emissiveIntensity: (val) => instance.emissiveIntensity !== undefined &&
|
|
43
|
-
(instance.emissiveIntensity = val),
|
|
44
|
-
roughness: (val) => instance.roughness !== undefined &&
|
|
45
|
-
(instance.roughness = val),
|
|
46
|
-
metalness: (val) => instance.metalness !== undefined &&
|
|
47
|
-
(instance.metalness = val),
|
|
48
29
|
};
|
|
30
|
+
// Color-type properties that need .set()
|
|
31
|
+
const colorKeys = new Set([
|
|
32
|
+
"color",
|
|
33
|
+
"emissive",
|
|
34
|
+
"specular",
|
|
35
|
+
"sheenColor",
|
|
36
|
+
"attenuationColor",
|
|
37
|
+
]);
|
|
49
38
|
for (const key in initialValues) {
|
|
50
39
|
const setter = propertyMap[key];
|
|
51
40
|
if (setter) {
|
|
52
41
|
setter(initialValues[key]);
|
|
53
42
|
}
|
|
43
|
+
else if (colorKeys.has(key)) {
|
|
44
|
+
const colorProp = instance[key];
|
|
45
|
+
if (colorProp && colorProp.set) {
|
|
46
|
+
colorProp.set(initialValues[key]);
|
|
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
|
+
}
|
|
55
|
+
else if (key in instance && typeof instance[key] === "number") {
|
|
56
|
+
instance[key] = initialValues[key];
|
|
57
|
+
}
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
// Store instance in the ref so animations can access it
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Vector3, Euler, Color, ReactThreeFiber } from '@react-three/fiber';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
|
-
import
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, ReactNode } from 'react';
|
|
4
5
|
import { MotionValue, MotionProps, ResolvedValues } from 'motion/react';
|
|
5
6
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
6
7
|
|
|
@@ -51,5 +52,43 @@ interface MotionCameraProps extends ThreeMotionProps {
|
|
|
51
52
|
}
|
|
52
53
|
declare const MotionCamera: (props: MotionCameraProps) => react_jsx_runtime.JSX.Element;
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
interface PresenceContextValue {
|
|
56
|
+
isPresent: boolean;
|
|
57
|
+
safeToRemove: () => void;
|
|
58
|
+
custom?: unknown;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns presence information for the current component.
|
|
62
|
+
*
|
|
63
|
+
* - `isPresent`: Whether the component is currently present in the tree.
|
|
64
|
+
* When `false`, the component is exiting and should play its exit animation.
|
|
65
|
+
* - `safeToRemove`: Call this function when the exit animation is complete
|
|
66
|
+
* to signal that the component can be unmounted.
|
|
67
|
+
*
|
|
68
|
+
* Returns `null` if the component is not wrapped in `AnimatePresence`.
|
|
69
|
+
*/
|
|
70
|
+
declare function usePresence(): [boolean, () => void] | null;
|
|
71
|
+
|
|
72
|
+
interface AnimatePresenceProps {
|
|
73
|
+
children?: ReactNode;
|
|
74
|
+
/**
|
|
75
|
+
* If `true`, `AnimatePresence` will only render one component
|
|
76
|
+
* at a time. The exiting component will finish its exit animation
|
|
77
|
+
* before the entering component is rendered.
|
|
78
|
+
*
|
|
79
|
+
* @default "sync"
|
|
80
|
+
*/
|
|
81
|
+
mode?: "sync" | "wait";
|
|
82
|
+
/**
|
|
83
|
+
* Fires when all exiting children have finished animating out.
|
|
84
|
+
*/
|
|
85
|
+
onExitComplete?: () => void;
|
|
86
|
+
/**
|
|
87
|
+
* Custom data to pass to exiting children via the presence context.
|
|
88
|
+
*/
|
|
89
|
+
custom?: unknown;
|
|
90
|
+
}
|
|
91
|
+
declare function AnimatePresence({ children, mode, onExitComplete, custom, }: AnimatePresenceProps): react.FunctionComponentElement<react.ProviderProps<PresenceContextValue | null>>[];
|
|
92
|
+
|
|
93
|
+
export { AnimatePresence, MotionCamera, motion, usePresence };
|
|
55
94
|
export type { AcceptMotionValues, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "r3f-motion",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "A simple and powerful React animation library for @react-three/fiber leveraging motion.dev",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/es/index.mjs",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"postpublish": "git push --tags"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@storybook/addon-docs": "^10.2.10",
|
|
53
54
|
"motion": "^12.29.0",
|
|
54
55
|
"react-merge-refs": "^3.0.2"
|
|
55
56
|
},
|