Images to → 3D Parallax implemented with web apis. Enjoy iOS 26 Spatial scene on the web. Inspired by DepthFlow.
demo-h264.mp4
Open depth-flow.pages.dev to create a new 3D parallax flow.
To use the depth-flow npm package, first create a flow zip file depth-flow.zip on the site.
import {
loadFlowZip,
createFlowSimpleRenderer,
flowSimpleRendererPresets,
} from "depth-flow"
import flowZipUrl from "./depth-flow.zip?url"
async function main() {
const canvas = document.createElement("canvas")
const blob = await fetch(flowZipUrl).then(res => res.blob())
const flow = await loadFlowZip(blob)
const renderer = await createFlowSimpleRenderer(canvas, flow, {
forwardSteps: 120,
backwardSteps: 8,
timer: "gl",
})
const origin: [number, number, number] = [0, 0, -30]
const target: [number, number, number] = [0, 0, 0]
const zoomScale = 0.9
let originChanged = false
const renderLoop = () => {
requestAnimationFrame(renderLoop)
renderer.timer.poll()
if (originChanged) {
renderer.render({ origin, target, zoomScale })
}
}
canvas.addEventListener("pointermove", event => {
const rect = canvas.getBoundingClientRect()
const x = (event.clientX - rect.left) / rect.width * -2 + 1
const y = (event.clientY - rect.top) / rect.height * 2 - 1
// or add spring animation here
origin[0] = x
origin[1] = y
originChanged = true
})
requestAnimationFrame(renderLoop)
canvas.style.position = "absolute"
canvas.style.top = "0"
canvas.style.left = "0"
canvas.style.width = "100%"
canvas.style.height = "100%"
const root = document.querySelector<HTMLDivElement>("#app")!
root.appendChild(canvas)
}
main()Render params details:
| Parameter | Type | Description |
|---|---|---|
origin |
[number, number, number] |
Camera position in 3D space. The x and y components can be controlled by pointer movement, and z can be controlled by mouse wheel |
target |
[number, number, number] |
Look-at target point. Fixed at [0,0,0] to look at the center of the scene |
zoomScale |
number |
Controls the zoom level/field of view. A value smaller than 1 to ensure the scene covers all the visible parts |
Renderer creation options:
| Parameter | Range | Default | Description |
|---|---|---|---|
forwardSteps |
16–256 |
120 |
Controls primary ray-marching quality and GPU cost |
backwardSteps |
1–16 |
8 |
Controls hit refinement quality |
edgeBlurThreshold |
0–0.25 |
0.05 |
Controls the soft blur at the image edge |
blurMipmapSize |
1–4096 |
200 |
Size of the precomputed edge-blur texture |
blurMipmapRadius |
0–100 |
10 |
Blur radius for that texture |
pixelRatio |
0.25–4 |
device pixel ratio | Canvas rendering resolution scale |
timer |
"noop" | "performance" | "gl" |
"noop" |
Optional draw-call timing mode |
Call renderer.dispose() when the canvas is no longer needed.
Timer modes:
"noop": default; no timing overhead."performance": measures CPU time around the draw call."gl": measures GPU draw-call time when the browser supportsEXT_disjoint_timer_query_webgl2; otherwise it falls back to noop.
Built-in presets are exported as flowSimpleRendererPresets:
const renderer = await createFlowSimpleRenderer(canvas, flow, "performance")Available presets: performance (64/4), balanced (120/8), and quality (180/12).
Preset objects remain available through flowSimpleRendererPresets for reuse or extension.
The whole scene is inside the box from [-1,-1,-1] to [1,1,1], and the bottom sits on the plane at [x,y,1]. The camera is expected to locate at a position with negative z value and look down through the z axis.
Checkout DepthFlow if you want to create a video.