Motion
Depth Image Card
A photo that parallaxes against the cursor by displacing its own pixels through a paired depth map, rendered on a three.js shader plane.
Move your cursor across it — the depth map drives the parallax
import DepthImageCard from "@/components/ui/depth-image-card";
export default function DepthImageCardDemo({ fullscreen }: { fullscreen?: boolean }) {
return (
<div className="w-full min-w-0 space-y-3">
<DepthImageCard
colorSrc="/kinematics/laptop.webp"
depthSrc="/kinematics/laptop-depth.png"
className={
fullscreen
? "h-dvh w-full"
: "aspect-video w-full overflow-hidden rounded-card border border-line bg-[#0b0b0f]"
}
/>
<p className="text-center text-xs text-faint">
Move your cursor across it — the depth map drives the parallax
</p>
</div>
);
}Installation
1. Install the dependencies.
terminal npm install three @react-three/fiber @react-three/drei clsx tailwind-merge2. Add the
cnhelper, if you don't already have it.lib/utils.ts import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }3. Copy the component into your project.
components/ui/depth-image-card.tsx 'use client' import { Canvas, useFrame, useThree } from '@react-three/fiber' import { useTexture } from '@react-three/drei' import { Suspense, useMemo, useRef, type PointerEvent } from 'react' import * as THREE from 'three' const vertexShader = /* glsl */ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } ` const fragmentShader = /* glsl */ ` uniform sampler2D uColorMap; uniform sampler2D uDepthMap; uniform vec2 uMouse; uniform float uStrength; varying vec2 vUv; void main() { float depth = texture2D(uDepthMap, vUv).r; vec2 offset = uMouse * uStrength * depth; vec2 uv = clamp(vUv + offset, 0.001, 0.999); gl_FragColor = texture2D(uColorMap, uv); } ` interface DepthPlaneProps { colorSrc: string depthSrc: string mouseTarget: React.RefObject<{ x: number; y: number }> } function DepthPlane({ colorSrc, depthSrc, mouseTarget }: DepthPlaneProps) { const [colorMap, depthMap] = useTexture([colorSrc, depthSrc]) const materialRef = useRef<THREE.ShaderMaterial>(null) const mouseRef = useRef({ x: 0, y: 0 }) const { viewport } = useThree() const uniforms = useMemo( () => ({ uColorMap: { value: colorMap }, uDepthMap: { value: depthMap }, uMouse: { value: new THREE.Vector2(0, 0) }, uStrength: { value: 0.05 }, }), [colorMap, depthMap] ) useFrame(() => { mouseRef.current.x += (mouseTarget.current.x - mouseRef.current.x) * 0.08 mouseRef.current.y += (mouseTarget.current.y - mouseRef.current.y) * 0.08 const mat = materialRef.current if (!mat) return mat.uniforms.uMouse.value.set(mouseRef.current.x, mouseRef.current.y) }) return ( <mesh scale={[viewport.width, viewport.height, 1]}> <planeGeometry args={[1, 1]} /> <shaderMaterial ref={materialRef} vertexShader={vertexShader} fragmentShader={fragmentShader} uniforms={uniforms} /> </mesh> ) } interface DepthImageCardProps { colorSrc: string depthSrc: string className?: string } /** Depth-map-driven parallax tilt — same technique as the hero's LaptopDepthCard, * minus the fluid-cursor reveal lens, for a plain photo + depth map pair. */ export default function DepthImageCard({ colorSrc, depthSrc, className }: DepthImageCardProps) { const mouseTarget = useRef({ x: 0, y: 0 }) const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => { const rect = e.currentTarget.getBoundingClientRect() const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1 const ny = ((e.clientY - rect.top) / rect.height) * 2 - 1 mouseTarget.current = { x: nx, y: -ny } } const handlePointerLeave = () => { mouseTarget.current = { x: 0, y: 0 } } return ( <div className={className} onPointerMove={handlePointerMove} onPointerLeave={handlePointerLeave} > <Canvas orthographic gl={{ antialias: true }} dpr={[1, 2]}> <Suspense fallback={null}> <DepthPlane colorSrc={colorSrc} depthSrc={depthSrc} mouseTarget={mouseTarget} /> </Suspense> </Canvas> </div> ) }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| colorSrc | string | — | The photo itself. |
| depthSrc | string | — | Greyscale depth map, same dimensions — white is near, black is far. |
| className | string | — | Sizes the card. The canvas fills it. |