3D Image Depth Map
Hover
Canvas
Three.js
GSAP
ScrollTrigger
JS
Preview Links
Custom JS
Enable custom code in preview or view on published site.
Enable custom code?
Last Updated: Mar 3, 2026
- Upload your image to generate a depth map version using websites like this one.
- Set the main image on the .depthmap-1_image
- Set the depth map image on the .depthmap-1_source
<script data-depthmap-1-shader="vertex" type="x-shader/x-vertex">
varying vec2 v_texcoord;
void main() {
v_texcoord = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script data-depthmap-1-shader="touch" type="x-shader/x-fragment">
precision mediump float;
uniform vec2 u_mouse;
uniform vec2 u_res;
uniform float u_time;
uniform sampler2D u_text;
uniform sampler2D u_map;
varying vec2 v_texcoord;
void main(){
vec2 uv = v_texcoord;
vec2 parallax = u_mouse * 0.035;
vec4 map = texture2D(u_map, uv);
float depthMap = map.r - 0.5;
vec2 offset = parallax * depthMap;
vec2 parallaxOffset = uv + offset;
vec4 color = texture2D(u_text, parallaxOffset);
gl_FragColor = color;
}
</script>
<script data-depthmap-1-shader="stamp" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D u_prev;
uniform vec2 u_cursor;
uniform vec2 u_prevCursor;
uniform float u_cursorSpeed;
uniform vec2 u_res;
uniform float u_fade;
uniform float u_canvasAspect;
varying vec2 v_texcoord;
float distToSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
void main() {
vec2 uv = v_texcoord;
float prev = texture2D(u_prev, uv).r * u_fade;
if (prev < 0.05) prev = 0.0;
vec2 compensate = u_canvasAspect >= 1.0
? vec2(1.0, u_canvasAspect)
: vec2(1.0 / u_canvasAspect, 1.0);
vec2 p = uv * compensate;
vec2 a = u_prevCursor * compensate;
vec2 b = u_cursor * compensate;
float dist = distToSegment(p, a, b);
float trailRadius = 0.08;
float brush = smoothstep(trailRadius, trailRadius * 0.6, dist);
float spot = brush * u_cursorSpeed * 0.4;
float result = max(prev, spot);
gl_FragColor = vec4(result, result, result, 1.0);
}
</script>
<script data-depthmap-1-shader="desktop" type="x-shader/x-fragment">
precision mediump float;
uniform vec2 u_mouse;
uniform vec2 u_res;
uniform float u_time;
uniform sampler2D u_text;
uniform sampler2D u_map;
uniform sampler2D u_trail;
uniform vec2 u_cursor;
uniform float u_cursorSpeed;
varying vec2 v_texcoord;
void main(){
vec2 uv = v_texcoord;
vec2 parallax = u_mouse * 0.035;
vec4 map = texture2D(u_map, uv);
float depthMap = map.r - 0.5;
vec2 offset = parallax * depthMap;
vec2 parallaxUV = uv + offset;
vec2 diff = parallaxUV - u_cursor;
vec2 pixelDiff = diff * u_res;
float dist = length(pixelDiff);
float bulgeRadius = 0.07 * max(u_res.x, u_res.y);
float bulgeStrength = 0.2 * u_cursorSpeed;
float bulge = smoothstep(bulgeRadius, 0.0, dist);
vec2 bulgeOffset = diff * bulge * bulgeStrength;
vec2 finalUV = parallaxUV - bulgeOffset;
vec4 color = texture2D(u_text, finalUV);
float trail = texture2D(u_trail, uv).r;
float boost = 1.0 + trail * 0.25;
color.rgb = min(color.rgb * boost, vec3(1.0));
gl_FragColor = color;
}
</script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/ScrollTrigger.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
var isTouch = false;
try {
isTouch = ("ontouchstart" in window) || (navigator.maxTouchPoints > 0);
} catch (e) {}
function easeMouse(normalized) {
var sign = normalized < 0 ? -1 : 1;
var abs = Math.abs(normalized);
return sign * Math.pow(abs, 0.25);
}
function getShader(name) {
var el = document.querySelector('[data-depthmap-1-shader="' + name + '"]');
return el ? el.textContent : "";
}
var vertexShader = getShader("vertex");
document.querySelectorAll(".depthmap-1_wrap").forEach(function (component, index) {
if (component.hasAttribute("data-depthmap-1")) return;
component.setAttribute("data-depthmap-1", "");
var canvas = component.querySelector(".depthmap-1_canvas");
var image = component.querySelector(".depthmap-1_image");
var source = component.querySelector(".depthmap-1_source");
if (!canvas || !image || !source) {
console.error("[depthmap-1] missing canvas, image, or source", component);
return;
}
var imageSrc = image.src;
var depthSrc = source.src;
var renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true });
renderer.setClearColor(0x000000, 0);
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
var scene = new THREE.Scene();
var lastWidth = 0;
var lastHeight = 0;
var imageWidth = 1;
var imageHeight = 1;
var loader = new THREE.TextureLoader();
var textTexture = loader.load(imageSrc, function (tex) {
imageWidth = tex.image.width;
imageHeight = tex.image.height;
updateScale();
});
var blurAmount = parseFloat(canvas.getAttribute("data-blur")) || 8;
function createBlurredTexture(src, blur, callback) {
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = function () {
var w = img.width;
var h = img.height;
var offscreen = document.createElement("canvas");
offscreen.width = w;
offscreen.height = h;
var ctx = offscreen.getContext("2d");
ctx.drawImage(img, 0, 0, w, h);
if (blur > 0) {
var imageData = ctx.getImageData(0, 0, w, h);
var pixels = imageData.data;
var passes = 3;
var radius = Math.round(blur);
for (var p = 0; p < passes; p++) {
var copy = new Uint8ClampedArray(pixels);
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var r = 0, g = 0, b = 0, a = 0, count = 0;
for (var dx = -radius; dx <= radius; dx++) {
var sx = Math.min(Math.max(x + dx, 0), w - 1);
var i = (y * w + sx) * 4;
r += copy[i];
g += copy[i + 1];
b += copy[i + 2];
a += copy[i + 3];
count++;
}
var idx = (y * w + x) * 4;
pixels[idx] = r / count;
pixels[idx + 1] = g / count;
pixels[idx + 2] = b / count;
pixels[idx + 3] = a / count;
}
}
copy = new Uint8ClampedArray(pixels);
for (var x2 = 0; x2 < w; x2++) {
for (var y2 = 0; y2 < h; y2++) {
var r2 = 0, g2 = 0, b2 = 0, a2 = 0, count2 = 0;
for (var dy = -radius; dy <= radius; dy++) {
var sy = Math.min(Math.max(y2 + dy, 0), h - 1);
var i2 = (sy * w + x2) * 4;
r2 += copy[i2];
g2 += copy[i2 + 1];
b2 += copy[i2 + 2];
a2 += copy[i2 + 3];
count2++;
}
var idx2 = (y2 * w + x2) * 4;
pixels[idx2] = r2 / count2;
pixels[idx2 + 1] = g2 / count2;
pixels[idx2 + 2] = b2 / count2;
pixels[idx2 + 3] = a2 / count2;
}
}
}
ctx.putImageData(imageData, 0, 0);
}
var tex = new THREE.CanvasTexture(offscreen);
tex.needsUpdate = true;
callback(tex);
};
img.onerror = function () {
console.error("Failed to load depth map:", src);
};
img.src = src;
}
var mapTexture = new THREE.Texture();
var mainUniforms = {
u_mouse: { value: new THREE.Vector2(0, 0) },
u_res: { value: new THREE.Vector2(canvas.clientWidth, canvas.clientHeight) },
u_time: { value: 0 },
u_text: { value: textTexture },
u_map: { value: mapTexture }
};
// --- Trail ping-pong (desktop only) ---
var trailTargetA, trailTargetB, stampScene, stampUniforms;
var trailSize = 512;
if (!isTouch) {
trailTargetA = new THREE.WebGLRenderTarget(trailSize, trailSize, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat
});
trailTargetB = new THREE.WebGLRenderTarget(trailSize, trailSize, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat
});
stampUniforms = {
u_prev: { value: trailTargetA.texture },
u_cursor: { value: new THREE.Vector2(0.5, 0.5) },
u_prevCursor: { value: new THREE.Vector2(0.5, 0.5) },
u_cursorSpeed: { value: 0 },
u_res: { value: new THREE.Vector2(trailSize, trailSize) },
u_fade: { value: 0.93 },
u_canvasAspect: { value: 1.0 }
};
var stampMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: getShader("stamp"),
uniforms: stampUniforms
});
stampScene = new THREE.Scene();
stampScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), stampMaterial));
mainUniforms.u_cursor = { value: new THREE.Vector2(0.5, 0.5) };
mainUniforms.u_cursorSpeed = { value: 0 };
mainUniforms.u_trail = { value: trailTargetB.texture };
}
createBlurredTexture(depthSrc, blurAmount, function (blurredTex) {
mapTexture.image = blurredTex.image;
mapTexture.needsUpdate = true;
mainUniforms.u_map.value = blurredTex;
});
var fragmentShaderSrc = isTouch
? getShader("touch")
: getShader("desktop");
var material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShaderSrc,
uniforms: mainUniforms,
transparent: true
});
var geometry = new THREE.PlaneGeometry(2, 2);
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
function updateScale() {
var canvasAspect = canvas.clientWidth / canvas.clientHeight;
var imageAspect = imageWidth / imageHeight;
var sx = imageAspect / canvasAspect;
var sy = 1;
if (sx < 1) {
sy /= sx;
sx = 1;
}
mesh.scale.set(sx, sy, 1);
}
// --- Input handling ---
if (isTouch) {
mainUniforms.u_mouse.value.set(0, 0);
var tracker = { val: 0 };
var lastDirection = 0;
var animating = false;
var scrollStopped = true;
var stopTimer = null;
function setUniform() {
var v = tracker.val;
mainUniforms.u_mouse.value.set(v * 0.35, v * 0.35);
}
function triggerAnimation(target) {
if (animating) return;
animating = true;
var tl = gsap.timeline({
onUpdate: setUniform,
onComplete: function () {
animating = false;
}
});
tl.to(tracker, {
val: target,
duration: 0.5,
ease: "power1.inOut"
});
tl.to(tracker, {
val: 0,
duration: 0.5,
ease: "power1.inOut"
});
}
ScrollTrigger.create({
trigger: component,
start: "clamp(top bottom)",
end: "clamp(bottom top)",
scrub: true,
onUpdate: function (self) {
var direction = self.direction;
var shouldTrigger = false;
if (direction !== lastDirection && lastDirection !== 0) {
scrollStopped = false;
shouldTrigger = true;
}
if (scrollStopped) {
scrollStopped = false;
shouldTrigger = true;
}
if (shouldTrigger) {
triggerAnimation(direction);
}
lastDirection = direction;
clearTimeout(stopTimer);
stopTimer = setTimeout(function () {
scrollStopped = true;
}, 150);
}
});
} else {
var smoothMouse = { x: 0, y: 0 };
var cursorUV = { x: 0.5, y: 0.5 };
var prevCursorUV = { x: 0.5, y: 0.5 };
var smoothSpeed = { val: 0 };
var writeToA = false;
document.addEventListener("mousemove", function (e) {
var rect = component.getBoundingClientRect();
if (
e.clientX < rect.left || e.clientX > rect.right ||
e.clientY < rect.top || e.clientY > rect.bottom
) return;
var nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;
var ny = ((e.clientY - rect.top) / rect.height) * 2 - 1;
var targetX = -easeMouse(nx) * 0.35;
var targetY = easeMouse(ny) * 0.35;
gsap.to(smoothMouse, {
x: targetX,
y: targetY,
duration: 0.8,
ease: "power2.out",
overwrite: true
});
cursorUV.x = (e.clientX - rect.left) / rect.width;
cursorUV.y = 1.0 - (e.clientY - rect.top) / rect.height;
});
}
// --- Render loop ---
function render(time) {
time *= 0.001;
var displayWidth = canvas.clientWidth;
var displayHeight = canvas.clientHeight;
if (displayWidth !== lastWidth || displayHeight !== lastHeight) {
lastWidth = displayWidth;
lastHeight = displayHeight;
var dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(displayWidth * dpr);
canvas.height = Math.round(displayHeight * dpr);
renderer.setViewport(0, 0, canvas.width, canvas.height);
mainUniforms.u_res.value.set(displayWidth, displayHeight);
if (!isTouch) {
stampUniforms.u_canvasAspect.value = displayWidth / displayHeight;
}
updateScale();
}
mainUniforms.u_time.value = time;
if (!isTouch) {
mainUniforms.u_mouse.value.set(smoothMouse.x, smoothMouse.y);
var rawVelX = cursorUV.x - prevCursorUV.x;
var rawVelY = cursorUV.y - prevCursorUV.y;
var rawSpeed = Math.sqrt(rawVelX * rawVelX + rawVelY * rawVelY);
var targetSpeed = Math.min(rawSpeed * 80, 1.0);
smoothSpeed.val += (targetSpeed - smoothSpeed.val) * 0.12;
if (smoothSpeed.val < 0.005) smoothSpeed.val = 0;
stampUniforms.u_prevCursor.value.set(prevCursorUV.x, prevCursorUV.y);
stampUniforms.u_cursor.value.set(cursorUV.x, cursorUV.y);
stampUniforms.u_cursorSpeed.value = smoothSpeed.val;
prevCursorUV.x = cursorUV.x;
prevCursorUV.y = cursorUV.y;
mainUniforms.u_cursor.value.set(cursorUV.x, cursorUV.y);
mainUniforms.u_cursorSpeed.value = smoothSpeed.val;
var readTarget = writeToA ? trailTargetB : trailTargetA;
var writeTarget = writeToA ? trailTargetA : trailTargetB;
stampUniforms.u_prev.value = readTarget.texture;
renderer.setRenderTarget(writeTarget);
renderer.render(stampScene, camera);
renderer.setRenderTarget(null);
mainUniforms.u_trail.value = writeTarget.texture;
writeToA = !writeToA;
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
updateScale();
requestAnimationFrame(render);
});
});
</script>









