Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MotionPathControls control via ref + add two properties #2354

Merged
merged 3 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions docs/controls/motion-path-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<THREE.Vector3>[]
curves?: THREE.Curve<THREE.Vector3>[];
/** 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<THREE.Object3D>
object?: React.MutableRefObject<THREE.Object3D>;
/** An object where the target looks towards, can also be a vector, default: null */
focus?: [x: number, y: number, z: number] | React.MutableRefObject<THREE.Object3D>
focus?: [x: number, y: number, z: number] | React.MutableRefObject<THREE.Object3D>;
/** 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;
}
```

Expand Down Expand Up @@ -114,3 +118,39 @@ function Loop() {
<cubicBezierCurve3 v0={[-5, -5, 0]} v1={[-10, 0, 0]} v2={[0, 3, 0]} v3={[6, 3, 0]} />
<Loop />
```

You can also use the MotionPathControls's reference to control the motion state in the `motion` property.

```tsx
const motionPathRef = useRef<MotionPathRef>(null!)
const motionPathObject = useRef<Mesh>(null!)

useFrame(() => {
if (motionPathRef.current) {
motionPathRef.current.motion.current += 0.01
}
})

<MotionPathControls
ref={motionPathRef}
object={motionPathObject}
curves={[
new THREE.CubicBezierCurve3(
new THREE.Vector3(-5, -5, 0),
new THREE.Vector3(-10, 0, 0),
new THREE.Vector3(0, 3, 0),
new THREE.Vector3(6, 3, 0)
),
new THREE.CubicBezierCurve3(
new THREE.Vector3(6, 3, 0),
new THREE.Vector3(10, 5, 5),
new THREE.Vector3(5, 3, 5),
new THREE.Vector3(5, 5, 5)
),
]}
/>
<mesh ref={motionPathObject}>
<planeGeometry args={[10, 10, 1, 1]} />
</mesh>
</MotionPathControls>
```
86 changes: 43 additions & 43 deletions src/core/MotionPathControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ type MotionPathProps = JSX.IntrinsicElements['group'] & {
curves?: THREE.Curve<THREE.Vector3>[]
/** 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<THREE.Object3D>
/** An object where the target looks towards, can also be a vector, default: null */
focus?: [x: number, y: number, z: number] | React.MutableRefObject<THREE.Object3D>
/** 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 */
Expand Down Expand Up @@ -45,46 +49,49 @@ type MotionState = {
next: THREE.Vector3
}

const isObject3DRef = (ref: any): ref is React.MutableRefObject<THREE.Object3D> =>
ref?.current instanceof THREE.Object3D
export type MotionPathRef = THREE.Group & { motion: MotionState }

const context = /* @__PURE__ */ React.createContext<MotionState>(null!)
const isObject3DRef = (ref: any): ref is React.MutableRefObject<THREE.Object3D> => ref?.current instanceof THREE.Object3D

const MotionContext = /* @__PURE__ */ React.createContext<MotionState>(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<THREE.Vector3[]>([])
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<THREE.Curve<THREE.Vector3>[]>([])

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) => (
<mesh key={index} material={material} geometry={geometry} position={[item.x, item.y, item.z]} />
))}
</>
)

return dots.map((item, index) => <mesh key={index} material={material} geometry={geometry} position={[item.x, item.y, item.z]} />)
}

export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group, MotionPathProps>(
export const MotionPathControls = /* @__PURE__ */ React.forwardRef<MotionPathRef, MotionPathProps>(
(
{
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,
Expand All @@ -94,10 +101,12 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group,
fref
) => {
const { camera } = useThree()
const ref = React.useRef<any>()
const [path] = React.useState(() => new THREE.CurvePath<THREE.Vector3>())

const pos = React.useRef(offset ?? 0)
const ref = React.useRef<MotionPathRef>(null!)
const pos = React.useRef<number>(offset ?? 0)

const path = React.useMemo(() => new THREE.CurvePath<THREE.Vector3>(), [])

const state = React.useMemo<MotionState>(
() => ({
focus,
Expand All @@ -114,10 +123,10 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group,

React.useLayoutEffect(() => {
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)
Expand All @@ -126,37 +135,28 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group,
path.updateArcLengths()
})

React.useImperativeHandle(fref, () => 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,
Expand All @@ -173,10 +173,10 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group,

return (
<group ref={ref} {...props}>
<context.Provider value={state}>
<MotionContext.Provider value={state}>
{children}
{debug && <Debug />}
</context.Provider>
{debug && <Debug color={debugColor} />}
</MotionContext.Provider>
</group>
)
}
Expand Down
Loading