<!--
Based on:
https://discourse.threejs.org/t/functions-to-calculate-the-visible-width-height-at-a-given-z-depth-from-a-perspective-camera/269/28
-->

<style>
	html,
	body,
	canvas {
		display: block;
		height: 100%;
		margin: 0;
		background-color: deeppink;
	}
</style>

<canvas></canvas>

<script type="module">
	import {
		WebGLRenderer,
		Scene,
		PerspectiveCamera,
		PlaneGeometry,
		Mesh,
		MeshBasicMaterial,
		TextureLoader
	} from "https://unpkg.com/three@0.154.0/build/three.module.js";

	const { atan, tan } = Math;

	// This is like the CSS `perspective` property (https://developer.mozilla.org/en-US/docs/Web/CSS/perspective)
	const perspective = 800; // start with perspective
	const V_FOV_DEG =
		  (180 * (2 * atan(window.innerHeight / 2 / perspective))) / Math.PI;
	// --- or ---
	// const V_FOV_DEG = 45; // start with fov
	// const perspective = ...solve for perspective...;

	const V_FOV_RAD = Math.PI * (V_FOV_DEG / 180);
	const TAN_HALF_V_FOV = tan(V_FOV_RAD / 2);

	const texture = new TextureLoader().load(
		"https://upload.wikimedia.org/wikipedia/commons/f/f4/The_Scream.jpg",

		// Don't render until we have the image loaded, otherwise if we render sooner, we will have unknown image size.
		onLoad
	);

	function onLoad() {
		const img = texture.image;
		const imgWidth = img.naturalWidth;
		const imgHeight = img.naturalHeight;
		const imgAspect = imgWidth / imgHeight;

		/** @param {'cover' | 'contain'} fitment */
		const getDistanceFromCamera = (fitment = "contain") => {
			const viewAspect = window.innerWidth / window.innerHeight;
			return (
				fitment === "contain" ? imgAspect <= viewAspect : imgAspect > viewAspect
			)
				? imgHeight / (2 * TAN_HALF_V_FOV) // Use the object size here! Not the viewport size!
			: imgWidth / (2 * tan(vfovToHfov(viewAspect) / 2));
		};

		const renderer = new WebGLRenderer({
			canvas: document.querySelector("canvas"),
			antialias: true,
			precision: "highp"
		});
		renderer.setPixelRatio(window.devicePixelRatio);

		const scene = new Scene();
		const camera = new PerspectiveCamera(V_FOV_DEG, 1, 100, 30000);
		scene.add(camera);

		const plane = new Mesh(
			new PlaneGeometry(imgWidth, imgHeight),
			new MeshBasicMaterial({
				map: texture
			})
		);
		scene.add(plane);

		function resize() {
			renderer.setSize(window.innerWidth, window.innerHeight, false);
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();

			// const distanceFromCam = getDistanceFromCamera("contain");
			// --- or ---
			const distanceFromCam = getDistanceFromCamera("cover");

			camera.position.z = distanceFromCam;
			// --- or ---
			// camera.position.z = perspective; // Like CSS, distance from camera to screen plane
			// plane.position.z = perspective - distanceFromCam; // Move the object instead

			// Now you might understand how CSS perspective works!
		}

		window.addEventListener("resize", resize);

		// Set initial values
		resize();

		requestAnimationFrame(function render() {
			renderer.render(scene, camera);
			requestAnimationFrame(render);
		});
	}

	/**
	 * @param {number} aspect - The camera aspect ratio, which is generally width/height of the viewport.
	 * @returns {number} - The horizontal field of view derived from the vertical field of view.
	 */
	function vfovToHfov(aspect) {
		return atan(aspect * TAN_HALF_V_FOV) * 2;
	}
</script>
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.