anamorphic — a small library
source on github ↗Look into the page.
The webcam finds your face, works out how far left, right, up and away from the screen you are sitting, and re-projects every layer of this page — and the room it is standing in — from that exact viewpoint. Lean, and you see around the inside corners of the box. Nothing is uploaded; it all runs in this tab.
See it in action on a real website. Visit sander.ame.link and click the Now in 3d!
detector none · eye +0mm, +0mm · 60cm away
The skull
Holbein hid one at the foot of The Ambassadors in 1533: a pale spindle lying across the floor tiles that means nothing from in front of the painting, and stands up as a skull once you get high to its right or low to its left. That is anamorphosis — a picture drawn correct for one viewpoint and wrong from everywhere else — and it is what this library is named after.
This one is wired to the tracker. It carries the inverse of the squeeze you would see from about 17 cm to the left of centre, so it is a smear until your eye arrives there. Start the camera and lean left, or drive it with the pointer.
The planes
Five, back to front. With it running, lean — they slide across each other by an amount set by how far apart they are and how far you moved. Sit square in front of the screen and they line up exactly as laid out.
far−340pxsignage on the back wall
deep−210pxset back
mid−110pxjust behind the glass
fore+95pxin front of it
near+185pxnearly in the room with you
Each plate is <div data-ana-plane="…">. That is the whole
markup change.
How it works
A plane at depth z, seen by an eye a distance P in front of the glass, projects as a uniform scale about the point where your line of sight crosses the screen. Content planes are all parallel to the screen, so that projection is exactly a scale plus a translate — no keystoning to fake, nothing that needs real 3D.
The trick that makes it usable on a real page is subtracting the resting projection — what you would see sitting square in front of the screen. What survives is only the differential, which is identical for every element on a plane and zero when your eye is centred. A head-on viewer sees the page pixel-for-pixel as it was laid out; it only moves relative to itself as they lean. Text stays crisp, hit targets stay put, nothing reflows.
The room behind the page is the one part that needs genuine CSS 3D — its
walls are rotated — so it gets a real perspective driven by the
same measurement. Distance comes from the interpupillary span: the iris
landmarks are a known ~63 mm apart, so their separation in pixels gives
range through a pinhole model.
s = P / (P - z) projection of a plane at depth z Δ = eyeOffsetPx · (1 - s) what is left after removing the rest pose
Use it
Install
npm install anamorphic
Compiled ESM with type declarations — no bundler required, nothing to transpile. The Svelte components ship as source, for hosts that already compile Svelte.
Anywhere — no framework
import 'anamorphic/anamorphic.css';
import { mountRoom, startCamera } from 'anamorphic';
mountRoom(); // draws the box, tears itself down when off
startCamera(); // asks, loads the mesh, starts tracking
Mark your planes
<div data-ana-page> <h1 data-ana-plane="fore">In front</h1> <p data-ana-plane="deep">Behind</p> </div>
Or from your own stylesheet
/* the z-index ordering is yours to keep straight if you do it this way */
.headline { transform: var(--ana-t-fore); position: relative; z-index: 4; }
.backdrop { transform: var(--ana-t-far); position: relative; z-index: 1; }
With Svelte, if you have it
import { AnaRoom, AnaHud, AnaInvite } from 'anamorphic/svelte';
Tune it
configure({
planes: [{ key: 'back', z: -400 }, { key: 'front', z: 120 }],
sizeCue: 0.3, // 0 = parallax only, 1 = true perspective scaling
ramp: 190, // ms to ease in and out
modelUrl: '/models/face_landmarker.task', // self-host the mesh
amplitude: (width, calm) => (calm ? 0.45 : width < 640 ? 0.6 : 1)
});
The full reference — every store, every option, the failure modes and the things
that bite — is in README.md.
What it costs
-
A secure context. Browsers hand out cameras on
https://andlocalhostonly. - ~3 MB once for the face mesh, fetched on first start. Point it at your own host to avoid the third parties.
- One animation frame loop while running, writing custom properties that are transforms — so the work stays on the compositor.
- No camera, no problem. The cursor or a phone's orientation sensors drive it just as well, and it falls back on its own when a camera is refused.
- Nothing leaves the tab. No frame, no landmark, no measurement.