An orthographic camera has no field of view, so “make it fit” stops being a lens decision and becomes one line of arithmetic.
A perspective camera always leaves you a way out. The frame is wrong, so you widen the field of view, or you back the camera off, and somewhere between those two dials is a combination that puts the subject where you wanted it. An orthographic projection has neither. Parallel lines stay parallel, distance does not change size, and moving the camera further away changes precisely nothing about how big anything appears.
What is left is a single scalar. zoom on a three.js orthographic camera is pixels per world unit, and it is the only thing standing between a room measured in metres and a viewport measured in CSS pixels. So the framing has to be written as a contract — here is a box of world units that must survive inside the canvas, whatever shape the canvas turns out to be — and the fit is whichever zoom honours it on both axes at once.
One angle, and only one
True isometric is not a phrase for “a nice three-quarter view”. It is azimuth 45° and elevation atan(1/√2), about 35.264°, and that second number is not a taste decision — it is the one elevation at which the three world axes project to equal lengths on screen. A unit along x, y and z all measure the same on the page, which is what lets the diorama read as a drawing of a room rather than a photograph of one.
Two degrees off and nothing looks broken; it simply stops being isometric. A rack unit 0.3 tall no longer measures the same as 0.3 of desk depth, and the eye files the whole thing as a mildly odd perspective render. So the config stores the expression and not the value. Math.atan(1 / Math.SQRT2) says why the number is what it is; 0.6155 would only say that somebody once measured something.
/** True isometric framing. Camera azimuth 45deg, elevation atan(1/sqrt2). */
export const ISO = {
azimuth: Math.PI / 4,
elevation: Math.atan(1 / Math.SQRT2), // ~35.264 deg
distance: 40,
// centred on the room so the WHOLE diorama fits inside its box with margin —
// it must never bleed off its edges or overlap the hero copy. y is lifted to
// the projected centroid so floor and wall-tops share the vertical padding.
target: [0, 3.2, 0] as const,
// world units that must fit the box (width / height). The room projects to
// ~19.8 x ~17.1 units; these carry a padding margin so nothing touches an
// edge. Larger box => bigger room, same composition (4K just sharper).
fitW: 21,
fitH: 18.5,
// phones show the room in a bottom band — centre on the room and frame the
// WHOLE diorama (incl. the car) so nothing important clips off the band
targetCompact: [0.4, 2.1, 0.5] as const,
fitWCompact: 20,
fitHCompact: 17.5,The box is authored, not measured
The room is a 14 × 14 floor with 7-unit walls. Turn it 45° and its screen width is the floor's own diagonal, 14√2 ≈ 19.8 units. Its screen height is that same diagonal foreshortened by sin(elevation) — about 11.4 — plus the wall height stood back up by cos(elevation), about 5.7. Which is exactly where the comment's ~19.8 × ~17.1 comes from: both numbers fall straight out of the angle, and neither is measured at runtime.
Against that, fitW and fitH are 21 and 18.5 — roughly 6% of horizontal margin and 8% of vertical. The asymmetry is deliberate: the wall tops sit close to the top edge and the hero copy is stacked over the same canvas, so the vertical needs more slack than the horizontal does. A computed bounding box would be more accurate and considerably worse, because it would retighten every time a prop moved and the framing would drift along with the furniture.
y is lifted to the projected centroid so floor and wall tops share the vertical paddingdistance at 40 that is 100 units either side of the target, so nothing clips as the camera swingsmin, and what max would cost you
Each axis proposes a zoom of its own: width / fitW and height / fitH. Taking the minimum means the more restrictive axis binds and the other is left with slack, so the box touches the frame in one direction and floats in the other. That slack is not waste. It is the only reason the room never bleeds off an edge.
// ---- THE FIT ----
// zoom is pixels per world unit. Each axis proposes one; `min` takes the
// more restrictive, which is the only choice that keeps the whole box
// inside the frame in BOTH orientations.
const zx = vw / box.fitW;
const zy = vh / box.fitH;
const zoom = p.useMax ? Math.max(zx, zy) : Math.min(zx, zy);
// the padded fit box
const fw = box.fitW * zoom;
const fh = box.fitH * zoom;
ctx.setLineDash([5, 5]);
ctx.strokeStyle = "rgba(10,10,10,.28)";
ctx.lineWidth = 1;
ctx.strokeRect(vx + (vw - fw) / 2, vy + (vh - fh) / 2, fw, fh);
ctx.setLineDash([]);max is the tempting mistake, because it fills the frame and a filled frame looks decisive in a screenshot. On a portrait window — a phone held upright, or a browser dragged narrow — the height ratio is the larger of the two, so max scales the room until it fills vertically and the left and right walls run off the sides. The diorama's entire argument is that it is a cutaway room; losing a wall to the frame edge loses the read.
Picking one axis outright is worse than either, because it is only ever right in one orientation. Bind to width and a tall window crops the wall tops; bind to height and a wide one crops the walls. min is the single rule that survives both without having to know which one it is in.
The same box on a phone and a 4K panel
The fit is recomputed every frame from the canvas's current pixel size, which is why the rig carries no resize handling of its own — size comes straight off the renderer's store, the next frame's baseZoom follows it, and the room grows continuously as you drag the browser edge.
useFrame((_, delta) => {
const cam = camRef.current;
if (!cam) return;
const fw = compact ? ISO.fitWCompact : ISO.fitW;
const fh = compact ? ISO.fitHCompact : ISO.fitH;
const baseZoom = Math.min(size.width / fw, size.height / fh);
if (!curTarget.current) curTarget.current = defaultTarget.clone();
if (curZoom.current === 0) curZoom.current = baseZoom; // first frame: no zoom-in from nothing
let goalT: THREE.Vector3;
let goalZoom: number;
let goalAz: number;
let goalEl: number;
if (focus) {
goalT = focus.target;
goalZoom = baseZoom * focus.zoom;
goalAz = focus.az;
goalEl = focus.el;
} else {
goalT = defaultTarget;
goalZoom = baseZoom;
goalAz = ISO.azimuth;
goalEl = ISO.elevation;
}goalZoom = baseZoom * focus.zoom is what keeps the push-ins portable. The monitor's close-up is 6.0, the rack's 3.75, the whiteboard's 3.15 — all multipliers, never absolute zooms. A push-in therefore crops the same fraction of the frame on a phone as on a 4K panel, because it never leaves the units the fit established.
The same property is why a larger display does not reframe anything. Twice the pixels means twice the zoom, which means the same 21 × 18.5 units of room drawn twice as large. 4K is not a different composition; it is this composition with more samples in it. The phone, by contrast, genuinely is a different one — a slightly tighter box on a lower centre, so the car still fits inside its band.
