Skip to content

Commit

Permalink
Merge branch 'master' into rc
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett committed Feb 19, 2025
2 parents f130383 + c9d1463 commit 2b89cae
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 50 deletions.
50 changes: 45 additions & 5 deletions docs/controls/motion-path-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ sourcecode: src/core/MotionPathControls.tsx
Motion path controls, it takes a path of bezier curves or catmull-rom curves as input and animates the passed `object` along that path. It can be configured to look upon an external object for staging or presentation purposes by adding a `focusObject` property (ref).

```tsx
type MotionPathProps = ThreeElements['group'] & {
type MotionPathProps = JSX.IntrinsicElements['group'] & {
/** An optional array of THREE curves */
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.RefObject<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.RefObject<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
/** Optionally smooth the curve, default: false */
Expand Down Expand Up @@ -83,9 +87,9 @@ type MotionState = {
/** The combined curve */
path: THREE.CurvePath<THREE.Vector3>
/** The focus object */
focus: React.RefObject<THREE.Object3D<THREE.Event>> | [x: number, y: number, z: number] | undefined
focus: React.MutableRefObject<THREE.Object3D<THREE.Event>> | [x: number, y: number, z: number] | undefined
/** The target object that is moved along the curve */
object: React.RefObject<THREE.Object3D<THREE.Event>>
object: React.MutableRefObject<THREE.Object3D<THREE.Event>>
/** The automated, 0-1 normalised and damped current goal position along curve */
offset: number
/** The current point on the curve */
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>
```
98 changes: 53 additions & 45 deletions src/core/MotionPathControls.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import * as THREE from 'three'
import * as React from 'react'
import { ThreeElements, useFrame, useThree, Instance } from '@react-three/fiber'
import { useFrame, useThree } from '@react-three/fiber'
import { easing, misc } from 'maath'

export type MotionPathProps = Omit<ThreeElements['group'], 'ref'> & {
type MotionPathProps = JSX.IntrinsicElements['group'] & {
/** An optional array of THREE curves */
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.RefObject<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.RefObject<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
/** Optionally smooth the curve, default: false */
Expand All @@ -32,9 +36,9 @@ type MotionState = {
/** The combined curve */
path: THREE.CurvePath<THREE.Vector3>
/** The focus object */
focus: React.RefObject<THREE.Object3D> | [x: number, y: number, z: number] | undefined
focus: React.MutableRefObject<THREE.Object3D> | [x: number, y: number, z: number] | undefined
/** The target object that is moved along the curve */
object: React.RefObject<THREE.Object3D>
object: React.MutableRefObject<THREE.Object3D>
/** The 0-1 normalised and damped current goal position along curve */
offset: number
/** The current point on the curve */
Expand All @@ -45,58 +49,67 @@ type MotionState = {
next: THREE.Vector3
}

const isObject3DRef = (ref: any): ref is React.RefObject<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,
maxSpeed = Infinity,
...props
},
}: MotionPathProps,
fref
) => {
const { camera } = useThree()
const ref = React.useRef<THREE.Group>(null!)
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 @@ -113,15 +126,10 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group,

React.useLayoutEffect(() => {
path.curves = []
const _curves =
curves.length > 0
? curves
: (ref.current as THREE.Group & { __r3f: Instance<THREE.Group> }).__r3f!.children.map(
(instance) => instance.object
)
for (var i = 0; i < _curves.length; i++) path.add(_curves[i])

//Smoothen curve
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
if (smooth) {
const points = path.getPoints(typeof smooth === 'number' ? smooth : 1)
const catmull = new THREE.CatmullRomCurve3(points)
Expand All @@ -130,18 +138,18 @@ 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
const lastOffset = state.offset

easing.damp(
pos,
'current',
Expand All @@ -152,15 +160,15 @@ export const MotionPathControls = /* @__PURE__ */ React.forwardRef<THREE.Group,
undefined,
eps
)
state.offset = misc.repeat(pos.current, 1)
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 @@ -177,10 +185,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

0 comments on commit 2b89cae

Please sign in to comment.