framer-motion-3d
Version:
A simple and powerful React animation library for @react-three/fiber
557 lines (538 loc) • 20.7 kB
JavaScript
;
var framerMotion = require('framer-motion');
var three = require('three');
var React = require('react');
var jsxRuntime = require('react/jsx-runtime');
var fiber = require('@react-three/fiber');
var reactMergeRefs = require('react-merge-refs');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
const setVector = (name, defaultValue) => (i) => (instance, value) => {
if (instance[name] === undefined) {
instance[name] = new three.Vector3(defaultValue);
}
const vector = instance[name];
vector.setComponent(i, value);
};
const setEuler = (name, defaultValue) => (axis) => (instance, value) => {
if (instance[name] === undefined) {
instance[name] = new three.Euler(defaultValue);
}
const euler = instance[name];
euler[axis] = value;
};
const setColor = (name) => (instance, value) => {
if (instance[name] === undefined) {
instance[name] = new three.Color(value);
}
instance[name].set(value);
};
const setScale = setVector("scale", 1);
const setPosition = setVector("position", 0);
const setRotation = setEuler("rotation", 0);
const setters = {
x: setPosition(0),
y: setPosition(1),
z: setPosition(2),
scale: (instance, value) => {
if (instance.scale === undefined) {
instance.scale = new three.Vector3(1);
}
const scale = instance.scale;
scale.set(value, value, value);
},
scaleX: setScale(0),
scaleY: setScale(1),
scaleZ: setScale(2),
rotateX: setRotation("x"),
rotateY: setRotation("y"),
rotateZ: setRotation("z"),
color: setColor("color"),
specular: setColor("specular"),
};
function setThreeValue(instance, key, values) {
if (key in setters) {
setters[key](instance, values[key]);
}
else {
if (key === "opacity" && !instance.transparent) {
instance.transparent = true;
}
instance[key] = values[key];
}
}
const readVector = (name, defaultValue) => (axis) => (instance) => {
const value = instance[name];
return value ? value[axis] : defaultValue;
};
const readPosition = readVector("position", 0);
const readScale = readVector("scale", 1);
const readRotation = readVector("rotation", 0);
const readers = {
x: readPosition("x"),
y: readPosition("y"),
z: readPosition("z"),
scale: readScale("x"),
scaleX: readScale("x"),
scaleY: readScale("y"),
scaleZ: readScale("z"),
rotateX: readRotation("x"),
rotateY: readRotation("y"),
rotateZ: readRotation("z"),
};
function readAnimatableValue(value) {
if (value === undefined) {
return;
}
else if (value instanceof three.Color) {
return value.getStyle();
}
else {
return value;
}
}
function readThreeValue(instance, name) {
return name in readers
? readers[name](instance)
: readAnimatableValue(instance[name]) || 0;
}
const axes = ["x", "y", "z"];
const valueMap = {
"position-x": "x",
"position-y": "y",
"position-z": "z",
"rotation-x": "rotateX",
"rotation-y": "rotateY",
"rotation-z": "rotateZ",
"scale-x": "scaleX",
"scale-y": "scaleY",
"scale-z": "scaleZ",
};
const scrapeMotionValuesFromProps = (props, prevProps) => {
const motionValues = {};
let key;
for (key in props) {
const prop = props[key];
if (framerMotion.isMotionValue(prop) || framerMotion.isMotionValue(prevProps[key])) {
const valueKey = valueMap[key] || key;
motionValues[valueKey] = prop;
}
else if (Array.isArray(prop)) {
for (let i = 0; i < prop.length; i++) {
const value = prop[i];
const prevValue = prevProps[key];
const prevArrayValue = Array.isArray(prevValue)
? prevValue[i]
: undefined;
if (framerMotion.isMotionValue(value) ||
(prevArrayValue !== undefined &&
framerMotion.isMotionValue(prevArrayValue))) {
const name = valueMap[`${key}-${axes[i]}`];
motionValues[name] = value;
}
}
}
}
return motionValues;
};
const createRenderState = () => ({});
class ThreeVisualElement extends framerMotion.VisualElement {
constructor() {
super(...arguments);
this.type = "three";
this.measureInstanceViewportBox = framerMotion.createBox;
}
readValueFromInstance(instance, key) {
return readThreeValue(instance, key);
}
getBaseTargetFromProps() {
return undefined;
}
sortInstanceNodePosition(a, b) {
return a.id - b.id;
}
removeValueFromRenderState() { }
scrapeMotionValuesFromProps(props, prevProps) {
return scrapeMotionValuesFromProps(props, prevProps);
}
build(state, latestValues) {
for (const key in latestValues) {
state[key] = latestValues[key];
}
}
renderInstance(instance, renderState) {
for (const key in renderState) {
setThreeValue(instance, key, renderState);
}
}
}
const createVisualElement = (_, options) => new ThreeVisualElement(options, {});
function useHover(isStatic, { whileHover, onHoverStart, onHoverEnd, onPointerOver, onPointerOut, }, visualElement) {
const isHoverEnabled = whileHover || onHoverStart || onHoverEnd;
if (isStatic || !visualElement || !isHoverEnabled)
return {};
return {
onPointerOver: (event) => {
var _a;
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive("whileHover", true);
onPointerOver && onPointerOver(event);
},
onPointerOut: (event) => {
var _a;
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive("whileHover", false);
onPointerOut && onPointerOut(event);
},
};
}
function useTap(isStatic, { whileTap, onTapStart, onTap, onTapCancel, onPointerDown, }, visualElement) {
const isTapEnabled = onTap || onTapStart || onTapCancel || whileTap;
const isPressing = React.useRef(false);
const cancelPointerEndListeners = React.useRef(null);
if (isStatic || !visualElement || !isTapEnabled)
return {};
function removePointerEndListener() {
var _a;
(_a = cancelPointerEndListeners.current) === null || _a === void 0 ? void 0 : _a.call(cancelPointerEndListeners);
cancelPointerEndListeners.current = null;
}
function checkPointerEnd() {
var _a;
removePointerEndListener();
isPressing.current = false;
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive("whileTap", false);
return !framerMotion.isDragActive();
}
function onPointerUp(event, info) {
if (!checkPointerEnd())
return;
/**
* We only count this as a tap gesture if the event.target is the same
* as, or a child of, this component's element
*/
onTap === null || onTap === void 0 ? void 0 : onTap(event, info);
}
function onPointerCancel(event, info) {
if (!checkPointerEnd())
return;
onTapCancel === null || onTapCancel === void 0 ? void 0 : onTapCancel(event, info);
}
return {
onPointerDown: framerMotion.addPointerInfo((event, info) => {
var _a;
removePointerEndListener();
if (isPressing.current)
return;
isPressing.current = true;
/**
* Only set listener to passive if there are no external listeners.
*/
const options = {
passive: !(onTapStart || onTap || onTapCancel || onPointerDown),
};
cancelPointerEndListeners.current = framerMotion.pipe(framerMotion.addPointerEvent(window, "pointerup", onPointerUp, options), framerMotion.addPointerEvent(window, "pointercancel", onPointerCancel, options));
(_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive("whileTap", true);
onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
onTapStart === null || onTapStart === void 0 ? void 0 : onTapStart(event, info);
}),
};
}
const useRender = (Component, props, ref, _state, isStatic, visualElement) => {
const visualProps = useVisualProps(props);
/**
* If isStatic, render motion values as props
* If !isStatic, render motion values as props on initial render
*/
return React.createElement(Component, {
ref,
...framerMotion.filterProps(props, false, false),
...visualProps,
onUpdate: props.onInstanceUpdate,
...useHover(isStatic, props, visualElement),
...useTap(isStatic, props, visualElement),
});
};
function useVisualProps(props) {
return React.useMemo(() => {
const visualProps = {};
for (const key in props) {
const prop = props[key];
if (framerMotion.isMotionValue(prop)) {
visualProps[key] = prop.get();
}
else if (Array.isArray(prop) && prop.includes(framerMotion.isMotionValue)) {
visualProps[key] = prop.map(framerMotion.resolveMotionValue);
}
}
return visualProps;
}, []);
}
const useVisualState = framerMotion.makeUseVisualState({
scrapeMotionValuesFromProps,
createRenderState,
});
const preloadedFeatures = {
...framerMotion.animations,
};
function custom(Component) {
return framerMotion.createRendererMotionComponent({
Component,
preloadedFeatures,
useRender,
useVisualState,
createVisualElement,
});
}
const componentCache = new Map();
/**
* @deprecated Motion 3D is deprecated.
*/
const motion = new Proxy(custom, {
get: (_, key) => {
!componentCache.has(key) && componentCache.set(key, custom(key));
return componentCache.get(key);
},
});
const MotionCanvasContext = React.createContext(undefined);
const devicePixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
const calculateDpr = (dpr) => Array.isArray(dpr)
? framerMotion.clamp(dpr[0], dpr[1], devicePixelRatio)
: dpr || devicePixelRatio;
/**
* This file contains a version of R3F's Canvas component that uses our projection
* system for layout measurements instead of use-react-measure so we can keep the
* projection and cameras in frame.
*
* https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/web/Canvas.tsx
*/
function Block({ set }) {
framerMotion.useIsomorphicLayoutEffect(() => {
set(new Promise(() => null));
return () => set(false);
}, []);
return null;
}
class ErrorBoundary extends React__namespace.Component {
constructor() {
super(...arguments);
this.state = { error: false };
}
componentDidCatch(error) {
this.props.set(error);
}
render() {
return this.state.error ? null : this.props.children;
}
}
ErrorBoundary.getDerivedStateFromError = () => ({ error: true });
function CanvasComponent({ children, fallback, tabIndex, id, style, className, events, ...props }, forwardedRef) {
/**
* Import existing contexts to pass through variants and MotionConfig from
* the DOM to the 3D tree. Shared variants aren't officially supported yet
* because the parent DOM tree fires effects before the 3D tree, whereas
* variants are expected to run from bottom-up in useEffect.
*/
const motionContext = React.useContext(framerMotion.MotionContext);
const configContext = React.useContext(framerMotion.MotionConfigContext);
const [forceRender] = framerMotion.useForceUpdate();
const layoutCamera = React.useRef(null);
const dimensions = React.useRef({
size: { width: 0, height: 0 },
});
const { size, dpr } = dimensions.current;
const containerRef = React.useRef(null);
const handleResize = () => {
const container = containerRef.current;
dimensions.current = {
size: {
width: container.offsetWidth,
height: container.offsetHeight,
},
};
forceRender();
};
// Set canvas size on mount
React.useLayoutEffect(handleResize, []);
const canvasRef = React__namespace.useRef(null);
const [block, setBlock] = React__namespace.useState(false);
const [error, setError] = React__namespace.useState(false);
// Suspend this component if block is a promise (2nd run)
if (block)
throw block;
// Throw exception outwards if anything within canvas throws
if (error)
throw error;
const root = React.useRef(null);
if (size.width > 0 && size.height > 0) {
if (!root.current) {
root.current = fiber.createRoot(canvasRef.current);
}
root.current.configure({
...props,
dpr: dpr || props.dpr,
size: { ...size, top: 0, left: 0 },
events: events || fiber.events,
}).render(jsxRuntime.jsx(ErrorBoundary, { set: setError, children: jsxRuntime.jsx(React__namespace.Suspense, { fallback: jsxRuntime.jsx(Block, { set: setBlock }), children: jsxRuntime.jsx(MotionCanvasContext.Provider, { value: {
dimensions,
layoutCamera,
requestedDpr: calculateDpr(props.dpr),
}, children: jsxRuntime.jsx(framerMotion.MotionConfigContext.Provider, { value: configContext, children: jsxRuntime.jsx(framerMotion.MotionContext.Provider, { value: motionContext, children: children }) }) }) }) }));
}
framerMotion.useIsomorphicLayoutEffect(() => {
const container = canvasRef.current;
return () => fiber.unmountComponentAtNode(container);
}, []);
return (jsxRuntime.jsx("div", { ref: containerRef, id: id, className: className, tabIndex: tabIndex, style: {
position: "relative",
width: "100%",
height: "100%",
overflow: "hidden",
...style,
}, children: jsxRuntime.jsx("canvas", { ref: reactMergeRefs.mergeRefs([canvasRef, forwardedRef]), style: { display: "block" }, children: fallback }) }));
}
/**
* @deprecated Motion 3D is deprecated.
*/
const MotionCanvas = React.forwardRef(CanvasComponent);
const calcBoxSize = ({ x, y }) => ({
width: framerMotion.calcLength(x),
height: framerMotion.calcLength(y),
top: 0,
left: 0,
});
function useLayoutCamera({ makeDefault = true }, updateCamera) {
const context = React.useContext(MotionCanvasContext);
framerMotion.invariant(Boolean(context), "No MotionCanvas detected. Replace Canvas from @react-three/fiber with MotionCanvas from framer-motion.");
const { dimensions, layoutCamera, requestedDpr } = context;
const advance = fiber.useThree((three) => three.advance);
const set = fiber.useThree((three) => three.set);
const camera = fiber.useThree((three) => three.camera);
const size = fiber.useThree((three) => three.size);
const gl = fiber.useThree((three) => three.gl);
const { visualElement: parentVisualElement } = React.useContext(framerMotion.MotionContext);
const measuredLayoutSize = React.useRef(undefined);
React.useLayoutEffect(() => {
measuredLayoutSize.current = size;
updateCamera(size);
advance(performance.now());
const projection = parentVisualElement === null || parentVisualElement === void 0 ? void 0 : parentVisualElement.projection;
if (!projection)
return;
/**
* When the projection of an element changes we want to update the camera
* with the projected dimensions.
*/
const removeProjectionUpdateListener = projection.addEventListener("projectionUpdate", (newProjection) => updateCamera(calcBoxSize(newProjection)));
/**
* When the layout of an element changes we want to update the renderer
* output to match the layout dimensions.
*/
const removeLayoutMeasureListener = projection.addEventListener("measure", (newLayout) => {
const newSize = calcBoxSize(newLayout);
let dpr = requestedDpr;
const { width, height } = dimensions.current.size;
const xScale = width / newSize.width;
const yScale = height / newSize.height;
const maxScale = Math.max(xScale, yScale);
dpr = framerMotion.clamp(0.75, 4, maxScale);
dimensions.current = {
size: { width: newSize.width, height: newSize.height },
dpr,
};
gl.setSize(newSize.width, newSize.height);
gl.setPixelRatio(dpr);
});
/**
* When a projection animation completes we want to update the camera to
* match the recorded layout of the element.
*/
const removeAnimationCompleteListener = projection.addEventListener("animationComplete", () => {
const { layoutBox } = projection.layout || {};
if (layoutBox) {
setTimeout(() => {
const newSize = calcBoxSize(layoutBox);
updateCamera(newSize);
dimensions.current = { size: newSize };
gl.setSize(newSize.width, newSize.height);
gl.setPixelRatio(requestedDpr);
}, 50);
}
});
return () => {
removeProjectionUpdateListener();
removeLayoutMeasureListener();
removeAnimationCompleteListener();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useLayoutEffect(() => {
const { current: cam } = layoutCamera;
if (makeDefault && cam) {
const oldCam = camera;
set(() => ({ camera: cam }));
return () => set(() => ({ camera: oldCam }));
}
}, [camera, layoutCamera, makeDefault, set]);
return { size, camera, cameraRef: layoutCamera };
}
fiber.extend({ PerspectiveCamera: three.PerspectiveCamera });
/**
* Adapted from https://github.com/pmndrs/drei/blob/master/src/core/PerspectiveCamera.tsx
*
* @deprecated Motion 3D is deprecated.
*/
const LayoutCamera = React.forwardRef((props, ref) => {
const { cameraRef } = useLayoutCamera(props, (size) => {
const { current: cam } = cameraRef;
if (cam && !props.manual) {
cam.aspect = size.width / size.height;
cam.updateProjectionMatrix();
}
});
return (jsxRuntime.jsx(motion.perspectiveCamera, { ref: reactMergeRefs.mergeRefs([cameraRef, ref]), ...props }));
});
fiber.extend({ OrthographicCamera: three.OrthographicCamera });
/**
* @deprecated Motion 3D is deprecated.
*/
const LayoutOrthographicCamera = React.forwardRef((props, ref) => {
const { size, cameraRef } = useLayoutCamera(props, (newSize) => {
const { current: cam } = cameraRef;
if (cam) {
cam.left = newSize.width / -2;
cam.right = newSize.width / 2;
cam.top = newSize.height / 2;
cam.bottom = newSize.height / -2;
cam.updateProjectionMatrix();
}
});
return (jsxRuntime.jsx(motion.orthographicCamera, { left: size.width / -2, right: size.width / 2, top: size.height / 2, bottom: size.height / -2, ref: reactMergeRefs.mergeRefs([cameraRef, ref]), ...props }));
});
/**
* @deprecated Motion 3D is deprecated.
*/
function useTime() {
const time = framerMotion.useMotionValue(0);
const { isStatic } = React.useContext(framerMotion.MotionConfigContext);
!isStatic && fiber.useFrame((state) => time.set(state.clock.getElapsedTime()));
return time;
}
exports.LayoutCamera = LayoutCamera;
exports.LayoutOrthographicCamera = LayoutOrthographicCamera;
exports.MotionCanvas = MotionCanvas;
exports.motion = motion;
exports.useTime = useTime;