UNPKG

framer-motion-3d

Version:

A simple and powerful React animation library for @react-three/fiber

89 lines (86 loc) 3.89 kB
import { useContext, useRef, useLayoutEffect } from 'react'; import { useThree } from '@react-three/fiber'; import { invariant, MotionContext, clamp, calcLength } from 'framer-motion'; import { MotionCanvasContext } from './MotionCanvasContext.mjs'; const calcBoxSize = ({ x, y }) => ({ width: calcLength(x), height: calcLength(y), top: 0, left: 0, }); function useLayoutCamera({ makeDefault = true }, updateCamera) { const context = useContext(MotionCanvasContext); invariant(Boolean(context), "No MotionCanvas detected. Replace Canvas from @react-three/fiber with MotionCanvas from framer-motion."); const { dimensions, layoutCamera, requestedDpr } = context; const advance = useThree((three) => three.advance); const set = useThree((three) => three.set); const camera = useThree((three) => three.camera); const size = useThree((three) => three.size); const gl = useThree((three) => three.gl); const { visualElement: parentVisualElement } = useContext(MotionContext); const measuredLayoutSize = useRef(undefined); 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 = 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 }, []); 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 }; } export { useLayoutCamera };