From eb004df19ca439f87c001365b525ead98acf9026 Mon Sep 17 00:00:00 2001 From: Riment <184847571+Riment@users.noreply.github.com> Date: Sun, 16 Feb 2025 22:53:28 +0000 Subject: [PATCH 1/3] feat: add MotionPathControls control via ref + add two properties docs: update MotionPathControls doc --- docs/controls/motion-path-controls.mdx | 60 +++++++++++++++--- src/core/MotionPathControls.tsx | 86 +++++++++++++------------- 2 files changed, 93 insertions(+), 53 deletions(-) diff --git a/docs/controls/motion-path-controls.mdx b/docs/controls/motion-path-controls.mdx index 60e1e59a2..09c29f735 100644 --- a/docs/controls/motion-path-controls.mdx +++ b/docs/controls/motion-path-controls.mdx @@ -14,25 +14,29 @@ Motion path controls, it takes a path of bezier curves or catmull-rom curves as ```tsx type MotionPathProps = JSX.IntrinsicElements['group'] & { /** An optional array of THREE curves */ - curves?: THREE.Curve[] + curves?: THREE.Curve[]; /** Show debug helpers */ - debug?: boolean + debug?: boolean; + /** Color of debug helpers */ + debugColor?: THREE.ColorRepresentation; /** The target object that is moved, default: null (the default camera) */ - object?: React.MutableRefObject + object?: React.MutableRefObject; /** An object where the target looks towards, can also be a vector, default: null */ - focus?: [x: number, y: number, z: number] | React.MutableRefObject + focus?: [x: number, y: number, z: number] | React.MutableRefObject; + /** Should the target object loop back to the start when reaching the end, default: true */ + loop?: boolean; /** Position between 0 (start) and end (1), if this is not set useMotion().current must be used, default: null */ - offset?: number + offset?: number; /** Optionally smooth the curve, default: false */ - smooth?: boolean | number + smooth?: boolean | number; /** Damping tolerance, default: 0.00001 */ - eps?: number + eps?: number; /** Damping factor for movement along the curve, default: 0.1 */ - damping?: number + damping?: number; /** Damping factor for lookAt, default: 0.1 */ - focusDamping?: number + focusDamping?: number; /** Damping maximum speed, default: Infinity */ - maxSpeed?: number + maxSpeed?: number; } ``` @@ -114,3 +118,39 @@ function Loop() { ``` + +You can also use the MotionPathControls's reference to control the motion state in the `motion` property. + +```tsx +const motionPathRef = useRef(null!) +const motionPathObject = useRef(null!) + +useFrame(() => { + if (motionPathRef.current) { + motionPathRef.current.motion.current += 0.01 + } +}) + + + + + + +``` \ No newline at end of file diff --git a/src/core/MotionPathControls.tsx b/src/core/MotionPathControls.tsx index a5584174e..1c68d4e31 100644 --- a/src/core/MotionPathControls.tsx +++ b/src/core/MotionPathControls.tsx @@ -8,10 +8,14 @@ type MotionPathProps = JSX.IntrinsicElements['group'] & { curves?: THREE.Curve[] /** Show debug helpers */ debug?: boolean + /** Color of debug helpers */ + debugColor?: THREE.ColorRepresentation /** The target object that is moved, default: null (the default camera) */ object?: React.MutableRefObject /** An object where the target looks towards, can also be a vector, default: null */ focus?: [x: number, y: number, z: number] | React.MutableRefObject + /** Should the target object loop back to the start when reaching the end, default: true */ + loop?: boolean /** Position between 0 (start) and end (1), if this is not set useMotion().current must be used, default: null */ offset?: number /** Optionally smooth the curve, default: false */ @@ -45,46 +49,49 @@ type MotionState = { next: THREE.Vector3 } -const isObject3DRef = (ref: any): ref is React.MutableRefObject => - ref?.current instanceof THREE.Object3D +export type MotionPathRef = THREE.Group & { motion: MotionState } -const context = /* @__PURE__ */ React.createContext(null!) +const isObject3DRef = (ref: any): ref is React.MutableRefObject => ref?.current instanceof THREE.Object3D + +const MotionContext = /* @__PURE__ */ React.createContext(null!) export function useMotion() { - return React.useContext(context) as MotionState + const context = React.useContext(MotionContext) + if (!context) throw new Error('useMotion hook must be used in a MotionPathControls component.') + return context } -function Debug({ points = 50 }: { points?: number }) { +function Debug({ points = 50, color = 'black' }: { points?: number; color?: THREE.ColorRepresentation }) { const { path } = useMotion() const [dots, setDots] = React.useState([]) - const [material] = React.useState(() => new THREE.MeshBasicMaterial({ color: 'black' })) - const [geometry] = React.useState(() => new THREE.SphereGeometry(0.025, 16, 16)) + + const material = React.useMemo(() => new THREE.MeshBasicMaterial({ color: color }), [color]) + const geometry = React.useMemo(() => new THREE.SphereGeometry(0.025, 16, 16), []) + const last = React.useRef[]>([]) + React.useEffect(() => { if (path.curves !== last.current) { setDots(path.getPoints(points)) last.current = path.curves } }) - return ( - <> - {dots.map((item: { x: any; y: any; z: any }, index: any) => ( - - ))} - - ) + + return dots.map((item, index) => ) } -export const MotionPathControls = /* @__PURE__ */ React.forwardRef( +export const MotionPathControls = /* @__PURE__ */ React.forwardRef( ( { children, curves = [], - object, debug = false, - smooth = false, + debugColor = 'black', + object, focus, + loop = true, offset = undefined, + smooth = false, eps = 0.00001, damping = 0.1, focusDamping = 0.1, @@ -94,10 +101,12 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef { const { camera } = useThree() - const ref = React.useRef() - const [path] = React.useState(() => new THREE.CurvePath()) - const pos = React.useRef(offset ?? 0) + const ref = React.useRef(null!) + const pos = React.useRef(offset ?? 0) + + const path = React.useMemo(() => new THREE.CurvePath(), []) + const state = React.useMemo( () => ({ focus, @@ -114,10 +123,10 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef { path.curves = [] - const _curves = curves.length > 0 ? curves : ref.current?.__r3f.objects - for (var i = 0; i < _curves.length; i++) path.add(_curves[i]) + const _curves = curves.length > 0 ? curves : (ref.current as any)?.__r3f?.objects ?? [] + for (let i = 0; i < _curves.length; i++) path.add(_curves[i]) - //Smoothen curve + // Smoothen curve if (smooth) { const points = path.getPoints(typeof smooth === 'number' ? smooth : 1) const catmull = new THREE.CatmullRomCurve3(points) @@ -126,37 +135,28 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef ref.current, []) + React.useImperativeHandle(fref, () => Object.assign(ref.current, { motion: state }), [state]) React.useLayoutEffect(() => { // When offset changes, normalise pos to avoid overshoot spinning pos.current = misc.repeat(pos.current, 1) }, [offset]) - let last = 0 - const [vec] = React.useState(() => new THREE.Vector3()) + const vec = React.useMemo(() => new THREE.Vector3(), []) useFrame((_state, delta) => { - last = state.offset - easing.damp( - pos, - 'current', - offset !== undefined ? offset : state.current, - damping, - delta, - maxSpeed, - undefined, - eps - ) - state.offset = misc.repeat(pos.current, 1) + const lastOffset = state.offset + + easing.damp(pos, 'current', offset !== undefined ? offset : state.current, damping, delta, maxSpeed, undefined, eps) + state.offset = loop ? misc.repeat(pos.current, 1) : misc.clamp(pos.current, 0, 1) if (path.getCurveLengths().length > 0) { path.getPointAt(state.offset, state.point) path.getTangentAt(state.offset, state.tangent).normalize() - path.getPointAt(misc.repeat(pos.current - (last - state.offset), 1), state.next) + path.getPointAt(misc.repeat(pos.current - (lastOffset - state.offset), 1), state.next) const target = object?.current instanceof THREE.Object3D ? object.current : camera target.position.copy(state.point) - //@ts-ignore + if (focus) { easing.dampLookAt( target, @@ -173,10 +173,10 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef - + {children} - {debug && } - + {debug && } + ) } From d66728372856e3ba9a0acaf7accd76358c787cf4 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Mon, 17 Feb 2025 01:37:22 -0600 Subject: [PATCH 2/3] chore: format with Prettier --- src/core/MotionPathControls.tsx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/MotionPathControls.tsx b/src/core/MotionPathControls.tsx index 1c68d4e31..694ac9ce8 100644 --- a/src/core/MotionPathControls.tsx +++ b/src/core/MotionPathControls.tsx @@ -51,7 +51,8 @@ type MotionState = { export type MotionPathRef = THREE.Group & { motion: MotionState } -const isObject3DRef = (ref: any): ref is React.MutableRefObject => ref?.current instanceof THREE.Object3D +const isObject3DRef = (ref: any): ref is React.MutableRefObject => + ref?.current instanceof THREE.Object3D const MotionContext = /* @__PURE__ */ React.createContext(null!) @@ -77,7 +78,9 @@ function Debug({ points = 50, color = 'black' }: { points?: number; color?: THRE } }) - return dots.map((item, index) => ) + return dots.map((item, index) => ( + + )) } export const MotionPathControls = /* @__PURE__ */ React.forwardRef( @@ -123,7 +126,7 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef { path.curves = [] - const _curves = curves.length > 0 ? curves : (ref.current as any)?.__r3f?.objects ?? [] + const _curves = curves.length > 0 ? curves : ((ref.current as any)?.__r3f?.objects ?? []) for (let i = 0; i < _curves.length; i++) path.add(_curves[i]) // Smoothen curve @@ -147,7 +150,16 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef { const lastOffset = state.offset - easing.damp(pos, 'current', offset !== undefined ? offset : state.current, damping, delta, maxSpeed, undefined, eps) + easing.damp( + pos, + 'current', + offset !== undefined ? offset : state.current, + damping, + delta, + maxSpeed, + undefined, + eps + ) state.offset = loop ? misc.repeat(pos.current, 1) : misc.clamp(pos.current, 0, 1) if (path.getCurveLengths().length > 0) { From c048c2df3bd5bb252a03253934fa723ed24115c3 Mon Sep 17 00:00:00 2001 From: Cody Bennett Date: Wed, 19 Feb 2025 06:47:18 -0600 Subject: [PATCH 3/3] chore: cleanup --- docs/controls/motion-path-controls.mdx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/controls/motion-path-controls.mdx b/docs/controls/motion-path-controls.mdx index 09c29f735..412d8893e 100644 --- a/docs/controls/motion-path-controls.mdx +++ b/docs/controls/motion-path-controls.mdx @@ -14,29 +14,29 @@ Motion path controls, it takes a path of bezier curves or catmull-rom curves as ```tsx type MotionPathProps = JSX.IntrinsicElements['group'] & { /** An optional array of THREE curves */ - curves?: THREE.Curve[]; + curves?: THREE.Curve[] /** Show debug helpers */ - debug?: boolean; + debug?: boolean /** Color of debug helpers */ - debugColor?: THREE.ColorRepresentation; + debugColor?: THREE.ColorRepresentation /** The target object that is moved, default: null (the default camera) */ - object?: React.MutableRefObject; + object?: React.MutableRefObject /** An object where the target looks towards, can also be a vector, default: null */ - focus?: [x: number, y: number, z: number] | React.MutableRefObject; + focus?: [x: number, y: number, z: number] | React.MutableRefObject /** Should the target object loop back to the start when reaching the end, default: true */ - loop?: boolean; + loop?: boolean /** Position between 0 (start) and end (1), if this is not set useMotion().current must be used, default: null */ - offset?: number; + offset?: number /** Optionally smooth the curve, default: false */ - smooth?: boolean | number; + smooth?: boolean | number /** Damping tolerance, default: 0.00001 */ - eps?: number; + eps?: number /** Damping factor for movement along the curve, default: 0.1 */ - damping?: number; + damping?: number /** Damping factor for lookAt, default: 0.1 */ - focusDamping?: number; + focusDamping?: number /** Damping maximum speed, default: Infinity */ - maxSpeed?: number; + maxSpeed?: number } ```