Motion
Hover over the capsule to see entities and lights animate with springs from the Motion animation library. Motion values drive each entity's position, rotation and scale, while a script applies animated intensities to the lights.
- Demo
- Code
MotionExample.jsx
// ↑ imports hidden
/**
* Three spring values that animate together as an [x, y, z] array.
*/
const useMotionVec3 = (initial, defaultValue = 0) => {
const x = useSpring(initial?.[0] ?? defaultValue);
const y = useSpring(initial?.[1] ?? defaultValue);
const z = useSpring(initial?.[2] ?? defaultValue);
const array = useTransform([x, y, z], ([xVal, yVal, zVal]) => [xVal, yVal, zVal]);
const animateArray = (target) => {
if (!target) return;
x.set(target[0] ?? x.get());
y.set(target[1] ?? y.get());
z.set(target[2] ?? z.get());
};
return { array, animateArray };
};
/**
* An Entity whose position, rotation and scale spring toward the values
* passed in the `animate` prop, driven by Motion spring values.
*/
const MotionEntity = ({ children, animate: animateProps, ...props }) => {
const position = useMotionVec3(props.position, 0);
const rotation = useMotionVec3(props.rotation, 0);
const scale = useMotionVec3(props.scale, 1);
const entityRef = useRef(null);
useEffect(() => {
if (animateProps) {
position.animateArray(animateProps.position);
rotation.animateArray(animateProps.rotation);
scale.animateArray(animateProps.scale);
}
}, [animateProps]);
useMotionValueEvent(position.array, 'change', ([x, y, z]) => {
entityRef.current?.setLocalPosition(x, y, z);
});
useMotionValueEvent(rotation.array, 'change', ([x, y, z]) => {
entityRef.current?.setLocalEulerAngles(x, y, z);
});
useMotionValueEvent(scale.array, 'change', ([x, y, z]) => {
entityRef.current?.setLocalScale(x, y, z);
});
return (
<Entity
ref={entityRef}
{...props}
position={position.array.get()}
rotation={rotation.array.get()}
scale={scale.array.get()}
>
{children}
</Entity>
);
};
/**
* A light whose intensity animates toward the `intensity` prop. A motion
* value tweens the intensity and a script applies it to the light every frame.
*/
const MotionLight = ({ intensity = 1, type = 'directional', transition = { duration: 0.2 }, ...props }) => {
const intensityMV = useMotionValue(intensity);
useEffect(() => {
animate(intensityMV, intensity, transition);
}, [intensity]);
class LightScript extends PcScript {
static scriptName = 'lightScript';
update() {
this.entity.light.intensity = intensityMV.get();
}
}
return (
<>
<Script script={LightScript} />
<Light {...props} type={type} />
</>
);
};
/**
* Rotates the entity toward the pointer position.
*/
class MouseRotatesEntity extends PcScript {
static scriptName = 'mouseRotatesEntity';
initialize() {
this.target = new Vec2();
this.current = new Vec2();
this.app.mouse?.on(EVENT_MOUSEMOVE, (e) => {
this.target.set(e.x, e.y).mulScalar(2).subScalar(1).divScalar(20);
});
}
update(dt) {
this.current.lerp(this.current, this.target, 0.4 * dt);
this.entity.setEulerAngles(this.current.y, this.current.x, 0);
}
}
const MotionScene = () => {
const app = useApp();
const { asset: envAtlas } = useEnvAtlas('/assets/environment.png');
const [hovered, setHovered] = useState(false);
const setCursor = (cursor) => {
app.graphicsDevice.canvas.style.cursor = cursor;
};
const onPointerOver = () => {
setCursor('pointer');
setHovered(true);
};
const onPointerOut = () => {
setCursor('auto');
setHovered(false);
};
if (!envAtlas) {
return null;
}
const rotation = [0, 0, 90];
const scale = hovered ? [1.2, 1.2, 1.2] : [1, 1, 1];
return (
<Entity>
<Entity name="camera" position={[0, 0, 5]}>
<Camera fov={45} />
<StaticPostEffects />
<MotionLight intensity={hovered ? 1.3 : 0.3} />
</Entity>
{/* Create some environment lighting */}
<Environment envAtlas={envAtlas} showSkybox={false} skyboxIntensity={0.4} />
{/* Create some additional lighting */}
<Entity rotation={[0, -45, 23]}>
<MotionLight intensity={hovered ? 1.3 : 0.3} color="red" />
</Entity>
<Entity rotation={[0, -45, -23]}>
<MotionLight intensity={hovered ? 1.3 : 0.3} color="blue" />
</Entity>
{/* Create a capsule button that animates when hovered */}
<MotionEntity
name="button"
onPointerOver={onPointerOver}
onPointerOut={onPointerOut}
animate={{ rotation, scale }}
>
<Render type="capsule" />
{/* Create a decoration that animates when hovered */}
<MotionEntity
name="decoration"
scale={[0, 0, 0]}
animate={{
scale: hovered ? [0.5, 0.5, 0.5] : [0, 0, 0],
position: hovered ? [0, 0, 0] : [0, 0, -1]
}}
>
<Script script={MouseRotatesEntity} />
<Entity position={[2, 1.9, -1]} rotation={[23, -34, 45]} scale={[0.8, 0.8, 0.8]}>
<Render type="box" />
</Entity>
<Entity position={[-2, -1.8, -1]} rotation={[43, 34, 0]} scale={[0.8, 0.8, 0.8]}>
<Render type="torus" />
</Entity>
<Entity position={[1.9, 0, -3]} scale={[0.8, 0.8, 0.8]}>
<Render type="sphere" />
</Entity>
<Entity position={[-1.9, 0, -1]} scale={[0.8, 0.8, 0.8]}>
<Render type="cone" />
</Entity>
</MotionEntity>
</MotionEntity>
{/* A soft gradient glow that fades in on hover */}
<div
style={{
position: 'absolute',
inset: '15%',
pointerEvents: 'none',
transition: 'opacity 0.5s',
opacity: hovered ? 0.5 : 0,
background: 'linear-gradient(to top right, #a855f7, #ec4899, #f97316)',
filter: 'blur(100px)'
}}
/>
{/* The headline overlay */}
<div
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none'
}}
>
<h1
style={{
color: '#fff',
fontSize: '3.5rem',
fontWeight: 700,
margin: 0,
transition: 'all 0.3s',
transform: `scale(${hovered ? 1.2 : 1})`,
opacity: hovered ? 0.8 : 1
}}
>
Hover
</h1>
</div>
</Entity>
);
};
const MotionExample = () => (
<Application>
<MotionScene />
</Application>
);
export default MotionExample;