anamorphic — head-tracked perspective for the web
The skull
Holbein hid one at the foot of<br>The Ambassadors<br>in 1533: a pale spindle lying across the floor tiles that means nothing from in<br>front of the painting, and stands up as a skull once you get high to its right or<br>low to its left. That is anamorphosis — a picture drawn correct for one viewpoint<br>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<br>see from about 17 cm to the left of centre, so it is a smear until your eye<br>arrives there. Start the camera and lean left, or drive it with the pointer.
a smear — lean left
The planes
Five, back to front. With it running, lean — they slide across each other by an<br>amount set by how far apart they are and how far you moved. Sit square in front<br>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 . That is the whole<br>markup change.
How it works
A plane at depth z, seen by an eye a distance P in front of<br>the glass, projects as a uniform scale about the point where your line of<br>sight crosses the screen. Content planes are all parallel to the screen, so<br>that projection is exactly a scale plus a translate — no keystoning<br>to fake, nothing that needs real 3D.
The trick that makes it usable on a real page is subtracting the resting<br>projection — what you would see sitting square in front of the screen. What<br>survives is only the differential, which is identical for every element on a<br>plane and zero when your eye is centred. A head-on viewer sees the page<br>pixel-for-pixel as it was laid out; it only moves relative to itself as they<br>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<br>walls are rotated — so it gets a real perspective driven by the<br>same measurement. Distance comes from the interpupillary span: the iris<br>landmarks are a known ~63 mm apart, so their separation in pixels gives<br>range through a pinhole model.
s = P / (P - z) projection of a plane at depth z<br>Δ = 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.<br>The Svelte components ship as source, for hosts that already compile Svelte.
Anywhere — no framework
import 'anamorphic/anamorphic.css';<br>import { mountRoom, startCamera } from 'anamorphic';
mountRoom(); // draws the box, tears itself down when off<br>startCamera(); // asks, loads the mesh, starts tracking
Mark your planes
In front<br>Behind
Or from your own stylesheet
/* the z-index ordering is yours to keep straight if you do it this way */<br>.headline { transform: var(--ana-t-fore); position: relative; z-index: 4; }<br>.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({<br>planes: [{ key: 'back', z: -400 }, { key: 'front', z: 120 }],<br>sizeCue: 0.3, // 0 = parallax only, 1 = true perspective scaling<br>ramp: 190, // ms to ease in and out<br>modelUrl: '/models/face_landmarker.task', // self-host the mesh<br>amplitude: (width, calm) => (calm ? 0.45 : width
The full reference — every store, every option, the failure modes and the things<br>that bite — is in README.md.
What it costs
A secure context. Browsers hand out cameras on https:// and<br>localhost only.
~3 MB once for the face mesh, fetched on first start. Point it at<br>your own host to avoid the third parties.
One animation frame loop while running, writing custom properties that<br>are transforms — so the work stays on the compositor.
No camera, no problem. The cursor or a phone's orientation sensors drive<br>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.
Liked this? I also make
Temporal.ist — the timesheet that writes itself
It notices when you save your work and builds the hours for you. No timers to<br>start, no Friday afternoon spent reconstructing the week, and no screenshots or<br>keystrokes leaving your machine. Accurate hours, ready to invoice — for<br>freelancers and agencies who make digital work.
Track this week automatically →