3D Image Depth Map

Hover
Canvas
Three.js
GSAP
ScrollTrigger
JS
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>