PROWAREtech

articles » current » javascript » three-js » move-camera-z-position-based-on-object-size

ThreeJS: Move Camera Z-position Based on Object Size to Make a Responsive App

How to move the camera distance, or z-position, to fit an object within the size of the canvas or screen; how make an object fit within the confines of the canvas regardless of aspect ratio; how to make a mobile responsive web application using THREE.js.

By default, THREE.js uses the canvas height. To make it work based on width or height, first set all the objects in the scene to be based around a z-position of ZERO and then use the following functions to move the camera back to a z-position where the main background object, like a plane, will fit the screen's width and height. This is to prevent an object of choice from overflowing the scene. It can be any object, and if the object changes size as it rotates then modify the size a bit to accommodate this. An example is a cube which is given below. Note that the object does not need to exist, it can be imaginary; the functions only need to know the size of the object to fit into the view of the camera.

See related: Cover scene with background


function cameraDistanceForRectangle(camera, objWidth, objHeight, canvasWidth, canvasHeight) {
	var aspectRatio = canvasWidth / canvasHeight, a, b;
	if(objWidth > objHeight) {
		a = Math.max(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
		b = Math.min(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
	}
	else {
		a = Math.min(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
		b = Math.max(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
	}
	return Math.max(a, b);
}
// NOTE: this is some extra code to slightly simplify the above function
function cameraDistanceForSquare(camera, squareObjectSize, canvasWidth, canvasHeight) {
	return cameraDistanceForRectangle(camera, squareObjectSize, squareObjectSize, canvasWidth, canvasHeight);
}

Try the code in this example out by clicking the HTML icon (the object is blue and the background is red to show what is happening):


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>THREE.js - Confined to a Square</title>
	<style>
		body {
			margin: 0;
			padding: 0;
		}
		canvas {
			display: block;
			width: 100%;
			height: 100vh;
		}
	</style>
</head>
<body>
	<canvas></canvas>
	<script src="/js/three.min.js"></script>
	<script type="text/javascript">
		function canvasSize(canvas) {
			return { width: canvas.offsetWidth, height: canvas.offsetHeight };
		}

		// NOTE: this is the function that does all the work
		function cameraDistanceForRectangle(camera, objWidth, objHeight, canvasWidth, canvasHeight) {
			var aspectRatio = canvasWidth / canvasHeight, a, b;
			if(objWidth > objHeight) {
				a = Math.max(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
				b = Math.min(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
			}
			else {
				a = Math.min(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
				b = Math.max(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
			}
			return Math.max(a, b);
		}
		// NOTE: this is some extra code to slightly simplify the above function
		function cameraDistanceForSquare(camera, squareObjectSize, canvasWidth, canvasHeight) {
			return cameraDistanceForRectangle(camera, squareObjectSize, squareObjectSize, canvasWidth, canvasHeight);
		}

		var canvas = document.getElementsByTagName("canvas")[0];
		var size = canvasSize(canvas);

		var scene = new THREE.Scene();
		scene.background = new THREE.Color(0xFF0000); // NOTE: make the background red to stand out
		scene.matrixWorldAutoUpdate = true;

		var planeSize = 1000; // NOTE: this is the size of the object that we want to move the camera distance to match

		var planeGeometry = new THREE.PlaneBufferGeometry(planeSize, planeSize, 64, 64); // NOTE: use a square plane as the background!!!
		var planeMaterial = new THREE.MeshBasicMaterial({ color: 0x6699CC });
		var plane = new THREE.Mesh(planeGeometry, planeMaterial); // NOTE: this blue plane will stay within the confines of the scene
		scene.add(plane);

		var cubeSize = planeSize * 0.25; // NOTE: make the cube a quarter the size of the plane

		var cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
		var cube = new THREE.Mesh(cubeGeometry, [
			new THREE.MeshBasicMaterial({ color: 0x000000 }),
			new THREE.MeshBasicMaterial({ color: 0x333333 }),
			new THREE.MeshBasicMaterial({ color: 0x666666 }),
			new THREE.MeshBasicMaterial({ color: 0x999999 }),
			new THREE.MeshBasicMaterial({ color: 0xCC9966 }), // front
			new THREE.MeshBasicMaterial({ color: 0xCCCCCC })
		]);
		cube.position.z = cubeSize * 1.1; // NOTE: position the cube just in front of the plane; it is close enough to the zero that the scene is based on
		scene.add(cube);

		var camera = new THREE.PerspectiveCamera(10, size.width / size.height, 1, 1000000);
		camera.aspect = size.width / size.height;
		camera.updateProjectionMatrix();
		scene.add(camera);
		camera.position.z = cameraDistanceForSquare(camera, planeSize, size.width, size.height);

		var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
		renderer.setPixelRatio(window.devicePixelRatio);
		renderer.setSize(size.width, size.height);

		function animate() {
			cube.rotation.x += 0.001;
			cube.rotation.y += 0.002;
			cube.rotation.z += 0.003;
			renderer.render(scene, camera);
			requestAnimationFrame(animate);
		}
		animate();

		function windowSize(withoutScrollBar) {
			var wid = 0;
			var hei = 0;
			if (typeof window.innerWidth != "undefined") {
				wid = window.innerWidth;
				hei = window.innerHeight;
			}
			else {
				if (document.documentElement.clientWidth == 0) {
					wid = document.body.clientWidth;
					hei = document.body.clientHeight;
				}
				else {
					wid = document.documentElement.clientWidth;
					hei = document.documentElement.clientHeight;
				}
			}
			return { width: wid - (withoutScrollBar ? (wid - document.body.offsetWidth + 1) : 0), height: hei };
		}

		if(window.addEventListener) {
			window.addEventListener("resize", function () {
				var win = windowSize();
				canvas.style.width = win.width + "px"; // NOTE: always change the canvas width when the window width that it is based on changes!!!
				canvas.style.height = win.height + "px";
				size = canvasSize(canvas);
				camera.position.z = cameraDistanceForSquare(camera, planeSize, size.width, size.height);
				camera.aspect = size.width / size.height;
				camera.updateProjectionMatrix();
				renderer.setPixelRatio(window.devicePixelRatio);
				renderer.setSize(size.width, size.height);
			});
		}
	</script>
</body>
</html>

Here is an example of the scene staying to the confines of the blue rectangle irrelevant of the left and right sides being longer or the top and bottom sides being longer:


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>THREE.js - Confined to a Rectangle</title>
	<style>
		body {
			margin: 0;
			padding: 0;
		}
		canvas {
			display: block;
			width: 100%;
			height: 100vh;
		}
	</style>
</head>
<body>
	<canvas></canvas>
	<script src="/js/three.min.js"></script>
	<script type="text/javascript">
		function canvasSize(canvas) {
			return { width: canvas.offsetWidth, height: canvas.offsetHeight };
		}

		// NOTE: this is the function that does all the work
		function cameraDistanceForRectangle(camera, objWidth, objHeight, canvasWidth, canvasHeight) {
			var aspectRatio = canvasWidth / canvasHeight, a, b;
			if(objWidth > objHeight) {
				a = Math.max(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
				b = Math.min(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
			}
			else {
				a = Math.min(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
				b = Math.max(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
			}
			return Math.max(a, b);
		}
		// NOTE: this is some extra code to slightly simplify the above function
		function cameraDistanceForSquare(camera, squareObjectSize, canvasWidth, canvasHeight) {
			return cameraDistanceForRectangle(camera, squareObjectSize, squareObjectSize, canvasWidth, canvasHeight);
		}

		var canvas = document.getElementsByTagName("canvas")[0];
		var size = canvasSize(canvas);

		var scene = new THREE.Scene();
		scene.background = new THREE.Color(0xFF0000); // NOTE: make the background red to stand out
		scene.matrixWorldAutoUpdate = true;

		var rectWidth = parseInt(prompt("Enter a rectangle width:", 1500)) || 1500;
		var rectHeight = parseInt(prompt("Enter a rectangle height:", 1000)) || 1000;
		var cubeSize = Math.min(rectWidth, rectHeight) / Math.sqrt(3);
		var cubeSizeRotating = cubeSize * Math.sqrt(3);

		var planeGeometry = new THREE.PlaneBufferGeometry(rectWidth, rectHeight, 2, 2); // NOTE: use a rectangle plane as the background!!!
		var planeMaterial = new THREE.MeshBasicMaterial({ color: 0x6699CC });
		var plane = new THREE.Mesh(planeGeometry, planeMaterial); // NOTE: this blue plane will stay within the confines of the scene
		scene.add(plane);

		var cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
		var cube = new THREE.Mesh(cubeGeometry, [
			new THREE.MeshBasicMaterial({ color: 0x000000 }),
			new THREE.MeshBasicMaterial({ color: 0x333333 }),
			new THREE.MeshBasicMaterial({ color: 0x666666 }),
			new THREE.MeshBasicMaterial({ color: 0x999999 }),
			new THREE.MeshBasicMaterial({ color: 0xCC9966 }), // front
			new THREE.MeshBasicMaterial({ color: 0xCCCCCC })
		]);
		var cubeZ = cubeSizeRotating / 2;
		cube.position.z = parseInt(prompt("Enter a distance for the cube to be from the background plane (0 is an interesting choice):", cubeZ)) || cubeZ; // NOTE: position the cube just in front of the plane; it is close enough to the zero that the scene is based on
		scene.add(cube);

		var camera = new THREE.PerspectiveCamera(parseInt(prompt("Enter the camera field of view angle (the wider the angle then the more into the scene the audience will be):", 5)) || 5, size.width / size.height, 1, 1000000);
		camera.aspect = size.width / size.height;
		camera.updateProjectionMatrix();
		scene.add(camera);
		camera.position.z = cameraDistanceForRectangle(camera, rectWidth, rectHeight, size.width, size.height);

		var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
		renderer.setPixelRatio(window.devicePixelRatio);
		renderer.setSize(size.width, size.height);

		function animate() {
			cube.rotation.x += 0.003;
			cube.rotation.y += 0.006;
			cube.rotation.z += 0.009;
			renderer.render(scene, camera);
			requestAnimationFrame(animate);
		}
		animate();

		function windowSize(withoutScrollBar) {
			var wid = 0;
			var hei = 0;
			if (typeof window.innerWidth != "undefined") {
				wid = window.innerWidth;
				hei = window.innerHeight;
			}
			else {
				if (document.documentElement.clientWidth == 0) {
					wid = document.body.clientWidth;
					hei = document.body.clientHeight;
				}
				else {
					wid = document.documentElement.clientWidth;
					hei = document.documentElement.clientHeight;
				}
			}
			return { width: wid - (withoutScrollBar ? (wid - document.body.offsetWidth + 1) : 0), height: hei };
		}

		if(window.addEventListener) {
			window.addEventListener("resize", function () {
				var win = windowSize();
				canvas.style.width = win.width + "px"; // NOTE: always change the canvas width when the window width that it is based on changes!!!
				canvas.style.height = win.height + "px";
				size = canvasSize(canvas);
				camera.position.z = cameraDistanceForRectangle(camera, rectWidth, rectHeight, size.width, size.height);
				camera.aspect = size.width / size.height;
				camera.updateProjectionMatrix();
				renderer.setPixelRatio(window.devicePixelRatio);
				renderer.setSize(size.width, size.height);
			});
		}
	</script>
</body>
</html>

Here is another example with just a cube in empty space that sticks to the confines of the screen, both width and height (this is largely due to the fact that the field of view is only 5°):


<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>THREE.js - Confined to a Rotating Cube</title>
	<style>
		body {
			margin: 0;
			padding: 0;
		}
		canvas {
			display: block;
			width: 100%;
			height: 100vh;
		}
	</style>
</head>
<body>
	<canvas></canvas>
	<script src="/js/three.min.js"></script>
	<script type="text/javascript">
		function canvasSize(canvas) {
			return { width: canvas.offsetWidth, height: canvas.offsetHeight };
		}

		// NOTE: this is the function that does all the work
		function cameraDistanceForRectangle(camera, objWidth, objHeight, canvasWidth, canvasHeight) {
			var aspectRatio = canvasWidth / canvasHeight, a, b;
			if(objWidth > objHeight) {
				a = Math.max(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
				b = Math.min(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
			}
			else {
				a = Math.min(objWidth, objHeight) / 2 / aspectRatio / Math.tan(Math.PI * camera.fov / 360);
				b = Math.max(objWidth, objHeight) / 2 / Math.tan(Math.PI * camera.fov / 360);
			}
			return Math.max(a, b);
		}
		// NOTE: this is some extra code to slightly simplify the above function
		function cameraDistanceForSquare(camera, squareObjectSize, canvasWidth, canvasHeight) {
			return cameraDistanceForRectangle(camera, squareObjectSize, squareObjectSize, canvasWidth, canvasHeight);
		}

		var canvas = document.getElementsByTagName("canvas")[0];
		var size = canvasSize(canvas);

		var scene = new THREE.Scene();
		scene.background = new THREE.Color(0x000000);
		scene.matrixWorldAutoUpdate = true;

		var cubeSize = 1000; // NOTE: this can be any number, but since it is the only object in the scene it does not matter
		var cubeSizeRotating = cubeSize * Math.sqrt(3); // NOTE: the cube's tips will just fit within the confines of the screen/canvas width

		var cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize); // NOTE: shrink the cube a bit to make sure it fits when rotating
		var cube = new THREE.Mesh(cubeGeometry, [
			new THREE.MeshBasicMaterial({ color: 0xFFFFFF }),
			new THREE.MeshBasicMaterial({ color: 0x333333 }),
			new THREE.MeshBasicMaterial({ color: 0x666666 }),
			new THREE.MeshBasicMaterial({ color: 0x999999 }),
			new THREE.MeshBasicMaterial({ color: 0xCC9966 }), // front
			new THREE.MeshBasicMaterial({ color: 0xCCCCCC })
		]);
		scene.add(cube);

		var camera = new THREE.PerspectiveCamera(5, size.width / size.height, 1, 1000000);
		camera.aspect = size.width / size.height;
		camera.updateProjectionMatrix();
		scene.add(camera);
		camera.position.z = cameraDistanceForSquare(camera, cubeSizeRotating, size.width, size.height);

		var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
		renderer.setPixelRatio(window.devicePixelRatio);
		renderer.setSize(size.width, size.height);

		function animate() {
			cube.rotation.x += 0.001;
			cube.rotation.y += 0.002;
			cube.rotation.z += 0.003;
			renderer.render(scene, camera);
			requestAnimationFrame(animate);
		}
		animate();

		function windowSize(withoutScrollBar) {
			var wid = 0;
			var hei = 0;
			if (typeof window.innerWidth != "undefined") {
				wid = window.innerWidth;
				hei = window.innerHeight;
			}
			else {
				if (document.documentElement.clientWidth == 0) {
					wid = document.body.clientWidth;
					hei = document.body.clientHeight;
				}
				else {
					wid = document.documentElement.clientWidth;
					hei = document.documentElement.clientHeight;
				}
			}
			return { width: wid - (withoutScrollBar ? (wid - document.body.offsetWidth + 1) : 0), height: hei };
		}

		if(window.addEventListener) {
			window.addEventListener("resize", function () {
				var win = windowSize();
				canvas.style.width = win.width + "px";
				canvas.style.height = win.height + "px";
				size = canvasSize(canvas);
				camera.position.z = cameraDistanceForSquare(camera, cubeSizeRotating, size.width, size.height);
				camera.aspect = size.width / size.height;
				camera.updateProjectionMatrix();
				renderer.setPixelRatio(window.devicePixelRatio);
				renderer.setSize(size.width, size.height);
			});
		}
	</script>
</body>
</html>

This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE