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: curve path animator #1651

Merged
merged 19 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
<li><a href="#presentationcontrols">PresentationControls</a></li>
<li><a href="#keyboardcontrols">KeyboardControls</a></li>
<li><a href="#FaceControls">FaceControls</a></li>
<li><a href="#motionpathcontrols">MotionPathControls</a></li>
</ul>
<li><a href="#gizmos">Gizmos</a></li>
<ul>
Expand Down Expand Up @@ -750,6 +751,75 @@ useFrame((_, delta) => {
})
```

#### MotionPathControls

<p>
<a href="https://codesandbox.io/s/drei-motion-path-controls-d9x4yf"><img width="20%" src="https://codesandbox.io/api/v1/sandboxes/drei-motion-path-n75jcq/screenshot.png" alt="Demo"/></a>
</p>

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 = JSX.IntrinsicElements['group'] & {
curves?: THREE.Curve[] // The curves from which the curve path is constructed, default: []
debug?: boolean // show the path on which the object animates, default: false
object?: React.MutableRefObject<THREE.Object3D> // default: default camera
focus?: [x: number, y: number, z: number] | React.MutableRefObject<THREE.Object3D> // default: undefined
offset?: number // manually progress the object along the path (0 - 1), default: undefined
smooth?: boolean // whether or not to smooth out the curve path, default: false
eps?: number // End of animation precision, default: 0.00001
damping?: number // Approximate time to reach the target. A smaller value will reach the target faster. default: 0.1
maxSpeed?: number // Optionally allows you to clamp the maximum speed. default: Infinity
}
```

```jsx
const poi = useRef()

function Loop({ factor = 0.2 }) {
const motion = useMotion()
useFrame((state, delta) => (motion.current += delta * factor))
}

<MotionPathControls
focus={poi}
damping={0.2}
>
<cubicBezierCurve3 v0={[-5, -5, 0]} v1={[-10, 0, 0]} v2={[0, 3, 0]} v3={[6, 3, 0]} />
<cubicBezierCurve3 v0={[6, 3, 0]} v1={[10, 5, 5]} v2={[5, 5, 5]} v3={[5, 5, 5]} />
<Loop />
</MotionPathControls>

<Box args={[1, 1, 1]} ref={poi}/>

```

```jsx
const poi = useRef()

<MotionPathControls
focus={poi}
damping={0.2}
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)
),
]}
/>

<Box args={[1, 1, 1]} ref={poi}/>
```


# Gizmos

#### GizmoHelper
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"lodash.clamp": "^4.0.3",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"maath": "^0.6.0",
"maath": "^0.9.0",
"meshline": "^3.1.6",
"react-composer": "^5.0.3",
"react-merge-refs": "^1.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/core/BBAnchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface BBAnchorProps extends GroupProps {
}

export const BBAnchor = ({ anchor, ...props }: BBAnchorProps) => {
const ref = React.useRef<THREE.Object3D>(null!)
const ref = React.useRef<THREE.Group>(null!)
const parentRef = React.useRef<THREE.Object3D | null>(null)

// Reattach group created by this component to the parent's parent,
Expand Down
2 changes: 1 addition & 1 deletion src/core/Cloud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function Cloud({
depthTest = true,
...props
}) {
const group = React.useRef<Group>()
const group = React.useRef<Group>(null!)
const cloudTexture = useTexture(texture) as Texture
const clouds = React.useMemo(
() =>
Expand Down
2 changes: 1 addition & 1 deletion src/core/CubeCamera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Props = Omit<JSX.IntrinsicElements['group'], 'children'> & {
} & CubeCameraOptions

export function CubeCamera({ children, frames = Infinity, resolution, near, far, envMap, fog, ...props }: Props) {
const ref = React.useRef<Group>()
const ref = React.useRef<Group>(null!)
const { fbo, camera, update } = useCubeCamera({
resolution,
near,
Expand Down
2 changes: 1 addition & 1 deletion src/core/GizmoHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const GizmoHelper = ({
// @ts-ignore
const defaultControls = useThree((state) => state.controls) as ControlsProto
const invalidate = useThree((state) => state.invalidate)
const gizmoRef = React.useRef<Group>()
const gizmoRef = React.useRef<Group>(null!)
const virtualCam = React.useRef<OrthographicCameraImpl>(null!)

const animating = React.useRef(false)
Expand Down
2 changes: 1 addition & 1 deletion src/core/MeshPortalMaterial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export const MeshPortalMaterial = React.forwardRef(

return (
<portalMaterialImpl
ref={ref}
ref={ref as any}
blur={blur}
blend={0}
resolution={[size.width * viewport.dpr, size.height * viewport.dpr]}
Expand Down
2 changes: 1 addition & 1 deletion src/core/MeshTransmissionMaterial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ export const MeshTransmissionMaterial: ForwardRefComponent<
<meshTransmissionMaterial
// Samples must re-compile the shader so we memoize it
args={[samples, transmissionSampler]}
ref={ref}
ref={ref as any}
{...props}
buffer={buffer || fboMain.texture}
// @ts-ignore
Expand Down
167 changes: 167 additions & 0 deletions src/core/MotionPathControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable prettier/prettier */
import * as THREE from 'three'
import * as React from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { easing, misc } from 'maath'

type MotionPathProps = JSX.IntrinsicElements['group'] & {
curves?: THREE.Curve<THREE.Vector3>[]
debug?: boolean
object?: React.MutableRefObject<THREE.Object3D>
focus?: [x: number, y: number, z: number] | React.MutableRefObject<THREE.Object3D>
offset?: number
smooth?: boolean
eps?: number
damping?: number
maxSpeed?: number
lookupDamping?: number
}

type MotionState = {
path: THREE.CurvePath<THREE.Vector3>
focus: React.MutableRefObject<THREE.Object3D<THREE.Event>> | [x: number, y: number, z: number] | undefined
object: React.MutableRefObject<THREE.Object3D<THREE.Event>>
current: number
offset: number
point: THREE.Vector3
tangent: THREE.Vector3
next: THREE.Vector3
}

const isObject3DRef = (ref: any): ref is React.MutableRefObject<THREE.Object3D> =>
ref?.current instanceof THREE.Object3D

const context = React.createContext<MotionState>(null!)

export function useMotion() {
return React.useContext(context) as MotionState
}

function Debug({ points = 50 }: { points?: number }) {
//@ts-ignore
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 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]} />
))}
</>
)
}

export const MotionPathControls = React.forwardRef<THREE.Group>(
(
{
children,
curves = [],
object,
debug = false,
smooth = false,
focus,
offset = undefined,
eps = 0.00001,
damping = 0.1,
lookupDamping = 0.1,
maxSpeed = Infinity,
...props
}: MotionPathProps,
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 state = React.useMemo<MotionState>(
() => ({
focus,
object: object?.current instanceof THREE.Object3D ? object : { current: camera },
path,
current: pos.current,
offset: pos.current,
point: new THREE.Vector3(),
tangent: new THREE.Vector3(),
next: new THREE.Vector3(),
}),
[focus, object]
)

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])

//Smoothen curve
if (smooth) {
const points = path.getPoints(typeof smooth === 'number' ? smooth : 1)
const catmull = new THREE.CatmullRomCurve3(points)
path.curves = [catmull]
}
path.updateArcLengths()
})

React.useImperativeHandle(fref, () => ref.current, [])

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())

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)

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)
const target = object?.current instanceof THREE.Object3D ? object.current : camera
target.position.copy(state.point)
//@ts-ignore
if (focus) {
easing.dampLookAt(
target,
isObject3DRef(focus) ? focus.current.getWorldPosition(vec) : focus,
lookupDamping,
delta,
maxSpeed,
undefined,
eps
)
}
}
})

return (
<group ref={ref} {...props}>
<context.Provider value={state}>
{children}
{debug && <Debug />}
</context.Provider>
</group>
)
}
)
2 changes: 1 addition & 1 deletion src/core/RoundedBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const RoundedBox: ForwardRefComponent<Props, Mesh> = React.forwardRef<Mes
}),
[depth, radius, smoothness]
)
const geomRef = React.useRef<ExtrudeGeometry>()
const geomRef = React.useRef<ExtrudeGeometry>(null!)

React.useLayoutEffect(() => {
if (geomRef.current) {
Expand Down
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export * from './PointerLockControls'
export * from './FirstPersonControls'
export * from './CameraControls'
export * from './FaceControls'
export * from './MotionPathControls'

// Gizmos
export * from './GizmoHelper'
Expand Down
4 changes: 2 additions & 2 deletions src/core/useGLTF.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Loader } from 'three'
// @ts-ignore
import { GLTFLoader, DRACOLoader, MeshoptDecoder, GLTF } from 'three-stdlib'
import { GLTFLoader, DRACOLoader, MeshoptDecoder } from 'three-stdlib'
import { useLoader } from '@react-three/fiber'

let dracoLoader: DRACOLoader | null = null
Expand Down Expand Up @@ -33,7 +33,7 @@ export function useGLTF<T extends string | string[]>(
useMeshOpt: boolean = true,
extendLoader?: (loader: GLTFLoader) => void
) {
const gltf = useLoader<GLTF, T>(GLTFLoader, path, extensions(useDraco, useMeshOpt, extendLoader))
const gltf = useLoader(GLTFLoader, path, extensions(useDraco, useMeshOpt, extendLoader))
return gltf
}

Expand Down
5 changes: 4 additions & 1 deletion src/web/View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,10 @@ export const View = ({ track, index = 1, frames = Infinity, children }: ViewProp
<group onPointerOver={() => null} />
</Container>,
virtualScene,
{ events: { compute, priority: index }, size: { width: rect.current?.width, height: rect.current?.height } }
{
events: { compute, priority: index },
size: { width: rect.current?.width, height: rect.current?.height } as any,
}
)}
</>
)
Expand Down
Loading