SOMETHING HAS TO STRETCH

Run the last flattening press on earth. Squash the globe into a map for people who each want a different lie.

Desktop:
Drag the globe sideways to spin it, down to press it flat. Set the brass arc and the gear wheel first — they pick which lie you tell.
Mobile:
Swipe the globe sideways to spin, down to press. Slide the brass arc, turn the wheel. Nobody gets all three needles.
Rate this game

No account needed.

Devlog

On Friday the UN General Assembly voted 164 to 1 to stop using Mercator as the picture of the world. Everybody ran the same headline: the map was wrong, now it's right. Almost nobody printed the part I keep thinking about, which is that the resolution exempts maritime and aerial navigation. Mercator keeps that job. The vote didn't decide Mercator was wrong. It decided Mercator was in the wrong room.

There is no correct map. Gauss proved it in 1827: a sphere cannot be flattened without distortion, not with better maths, not ever. You don't get to choose whether to lie about the world. You choose which lie, and where to put it.

So I built a machine that physically cannot make an honest map, and handed it over without explaining any of that.

The needles are real

The three brass gauges on the front read AREA, ANGLE and SHAPE, and I wanted them measured rather than scripted, because a difficulty curve dressed up as geometry would be the one dishonest thing in a game about honesty.

The press runs one continuous family of projections with two dials:

y(lat) = atanh(k · sin lat) / k
x(lon) = lon · (1 − κ · k² · sin² lat)

At k → 0 the first line is sin lat, which is Lambert cylindrical equal-area. At k → 1 it is atanh(sin lat), which is Mercator. Everything between is a real projection, finite, analytic, and differentiable, which matters because the needles read its derivatives. The second factor is not decoration: at κ = 1 it is exactly the reciprocal of the areal scale the first factor introduces, so the whole κ = 1 edge of the dial space is equal-area for any k at all. The player can reach equal-area. The player can reach conformal. They meet nowhere.

AREA is the land-weighted spread of the Tissot areal scale over 900 land cells. ANGLE is the land-weighted mean angular deformation, which is zero only where the projection is conformal. SHAPE fits each continent's projected outline against how that continent looks from directly above it on the globe, by least squares over a similarity transform, and reports the residual.

I swept the whole dial space in Node before writing a line of shader, and it caught two things I would otherwise have shipped. SHAPE could never exceed 41 out of 100, because a continent sixty degrees across can never match its own globe silhouette exactly and I was scoring against an unreachable zero. And AREA and ANGLE could both reach 93 at once, which would have quietly broken the only rule the game has. After rewriting ANGLE off the angular deformation instead of a rhumb test: max AREA 100, max ANGLE 95, max SHAPE 100, best pair 78, best all three 45. The impossible thing is impossible.

The nicest result came out of the maths rather than the design doc. SHAPE Procrustes-fits each continent, so a continent the seam runs through scores garbage, which means the central meridian matters enormously. With the tear in the Pacific, SHAPE tops out at 100. With the tear through Africa, at 13. The strategy the design asked for, spin the globe so the seam lands in open water instead of through somebody's country, fell straight out of the honest measurement instead of being written in.

One mesh, two objects

The globe and the map are the same thing: a 217 by 109 lat/lon grid whose vertices are placed on a sphere or on the printed sheet, per vertex, by the same projection the needles are measured from. The morph runs front to back rather than all at once, so the leading edge of the sheet is already flat and already paper while the back of the world is still a lacquered dome. Both worlds ride in one stacked texture, the school globe in the upper half and the printed sheet in the lower, and a single varying decides how far each fragment has travelled from one to the other.

Two hours in the dark

Then the map printed and it was invisible.

Not missing. Invisible. The geometry was right, and I proved it with a wireframe pass. The UVs were right, and I proved it with a checkerboard. The texture was right, and I dumped it out of the GPU to look at it. Every measurement said the sheet was exactly where and what it should be, and the delivery board showed dark green felt.

It was two bugs stacked, which is why every single test came back innocent.

The first: spinning the globe wrapped longitude by the central meridian, so the mesh's columns reordered. At the wrap point that leaves one column of quads stretched across the entire map, a single enormous polygon lying on top of the printed sheet like a dust cover. Every screenshot I took of "the blank map" was a screenshot of that quad. It is gone now; the mesh's longitude is fixed and spinning rotates map.offset.x instead, which is the same picture and has no seam quad in it.

Underneath that, the real one. The grid's winding is front-facing while it is a sphere and back-facing once it is flattened, because flattening turns the surface inside out relative to the camera. With THREE.DoubleSide, three flips the shading normal on back-facing fragments, so the printed map was being lit from underneath by a lamp that was above it. A correct sheet, correct texture, correct normal in object space, rendered black. The fix is one line telling the fragment shader to keep the normal it was given.

I found it by forcing an emissive constant onto the sheet. It lit up instantly at 201 out of 255, which meant the fragments were being drawn and the light was simply never arriving, and after two hours of measuring the wrong half of the problem that was the first number that pointed anywhere.

A sphere is an honest map of itself

For most of the build the three needles read the projection the dials were set to, all the time, whether or not anything had been pressed. It measured correctly and it was dead in the hand: the one verb the whole game is built on, dragging the world flat, moved no instrument at all.

It was also a lie. Nothing is distorted on a globe. Every area is its own area, every angle holds, Greenland is Greenland. AREA, ANGLE and SHAPE all belong on 100 while the world is still a sphere, and the distortion is not a property of the projection sitting in a dial somewhere. It is something the player manufactures by pulling.

So the needles start pegged and fall as the world flattens, each to a different place, and none of them all the way back. The argument stopped being a number on a gauge and became a thing you feel in your thumb: three needles leaving 100 together and refusing to arrive anywhere near each other. Setting a dial still swings them for a second so the machine can be aimed, then they drift back up to honest, because until the lever moves nothing has been lied about yet.

The ocean I had to paint the wrong colour

The globe kept arriving as a beach ball. Not the geometry, not the coastlines, the material. Against the near black of the room it read as inflatable vinyl, and I could not see why, because the sea in the paint file was a perfectly reasonable varnished teal.

So I sampled the running frame instead of trusting my eyes. Albedo #3f93ad, which is (63, 147, 173). On screen: (53, 151, 130). Blue down a quarter, green up. Thirty degrees of hue, straight into mint.

The lamp did it. The key light in this room is a tungsten bulb in a shade, and I gave it #ffd0a0, which carries 63 percent as much blue as red because that is what tungsten does. Multiply a blue albedo by a warm light and the blue channel is the one that pays. Then ACES pulls saturation out of the highlights on the way to the display, and the crown of a sphere is nothing but highlight. Two entirely correct pieces of physics, stacked, eating the only saturated object in the frame.

The fix is not to cool the lamp, because the warm pool of light is the whole look. It is to paint the sea at #3e86e0, a blue that is obviously too blue in the file and correct on the glass. Texture painting under a coloured key is inverse work: the number that matters is the one at the end of the pipeline, and every stop before it has an opinion.

I only found it by measuring. I had been staring at that globe for hours.

What mass does when it leaves

The squash started out volume preserving, which felt like the honest choice. Take a sphere, scale its height by s, widen its waist by 1/sqrt(s), and the volume is conserved exactly.

It looks wrong. By half travel the dome is 1.6 times wider than the globe was, which puts it outside the roller gap, past the frame of the press it is supposedly inside, and off the bottom of a phone screen. The maths is right and the picture is nonsense, because the assumption is false. Mass is not being conserved in that dome. It is leaving through the rollers as paper. Whatever the dome gives up in height mostly does not come back at the waist at all, it goes out the front as a printed sheet.

The waist now takes pow(1/s, 0.22). The dome ends about a third wider, stays inside the gap, and the sheet is what grows. Physical honesty is a tool, not a rule; the thing to be honest about was where the mass went.

The game was mute and I could not hear it

There are thirteen machine sounds in here, all synthesised on the fly: a four layer crush groan, the detent click, the thunk, paper, the roller drone. Late on I ran a full ten round shift with AudioContext.prototype instrumented from the page, counting every node the game asked for.

Zero. Not a quiet mix. Nothing had ever made a sound, all night, and I had watched hundreds of frames go by without noticing, because I was reading screenshots.

Two guards, each perfectly reasonable, stacked into silence. The audio rig hands back an object built once and kept in a useMemo. Its dispose sets a dead flag so a torn down rig can never schedule into a closed context. React 18 in development mounts an effect, tears it down, and mounts it again on purpose, to catch exactly this class of bug. The same object comes back for the second mount, and it is already dead.

That alone would have been a five second fix. The second guard is why it survived: the teardown React runs first happens before anything has played, so rig is still null, and dispose takes its if (!rig) return early exit two lines in. Any reset written at the bottom of that function is unreachable in precisely the case that matters.

dispose now returns the rig to cold rather than to dead, and it does it before the early exit. Same script, same shift: 343 oscillators, 283 sample sources, 1057 gains.

The lesson I actually take from it is not about React. It is that I had spent the whole build looking at this game and never once listening to it, and a still frame will lie to you about a machine that is supposed to thunk.

The argument was off the bottom of the screen

The whole game is three needles that refuse to agree. AREA, ANGLE, SHAPE, brass, sprung, measured off the real projection, clacking off their end stops. I built them early and I was proud of them.

Late on I stopped reading desktop captures and framed the machine the way it actually ships, 390 by 844, the size of a phone in one hand. All three dials were below the bottom edge of the canvas. Not clipped, not dim. Off the screen. The portrait camera aimed at y = 0.04 from just under seven units back, and everything under the bed fell out of frame, so the bottom quarter of a phone screen held an empty green felt bed while the instruments hung in the dark underneath the world.

Nine rounds of a game whose only argument is that you can peg any two needles and never three, on a device where there are no needles.

I had looked at this thing all night. I had measured the ocean's albedo to a byte and solved a separate glass highlight for each of the three dials from where the key lamp actually sits, on gauges nobody holding a phone could see. Landscape shows them, half cut off at the bottom edge, and landscape is the frame I had been reviewing in.

The fix is two numbers. Drop the aim onto the machine's own centre, pull back far enough that a tall frame holds crown, globe, rollers, bed and the row of dials at once. A portrait screen had the room the entire time. It was pointed at the wrong part of the press.

Getting them into the frame was not the same as getting them read. In frame they were a forty-four pixel row with labels four CSS pixels tall, which is a smudge, and the printed sheet slid in front of them at exactly the moment the needles start fighting. They are half again as big now, scaled about their own centres so they grow in place instead of sliding down through the apron, and lifted until the row sits clear of the paper. AREA, ANGLE and SHAPE, legible at arm's length, still disagreeing.

The varnish that erased the world

A school globe is paint under varnish, and that is two specular lobes, not one. A single roughness lobe can be glossy or it can be matte, but either way the light comes off the same surface the colour is on, which is exactly what makes painted plastic look like painted plastic. So I put a clearcoat on it: a thin dielectric sheet over the top with its own roughness and its own Fresnel.

The globe turned into a black mirror ball. No continents, no ocean, nothing. A dark sphere with the workshop window reflected in it.

The coat normal is the reason. three seeds clearcoatNormal from nonPerturbedNormal, and on a double sided material nonPerturbedNormal has already been multiplied by gl_FrontFacing. The final composite is

outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat;

Fcc is Schlick on the angle between the eye and that coat normal. Point it away from the eye and the Fresnel term goes to clearcoatF90, which is 1.0, which multiplies every diffuse photon on the sphere by zero. A varnish layer that deletes the object it is varnishing.

My mesh already carries an analytic normal computed in the vertex shader, because the same grid is a sphere at one end of the morph and a flat sheet at the other and the winding flips on the way. Handing that normal to the coat as well as to the paint fixes it in one line. The coat then gets dissolved by the very same varying that swaps the lacquered globe texture for the printed one, because paper coming out of the rollers is not varnished, and a wet gloss on the delivered map stops it being paper.

The comment was lying and I believed it

PRESS_GAIN sat at 1.5 with this directly above it:

// A full press is about 40% of the screen's height.

It wasn't. R3F hands you a pointer position normalised over two units of canvas, not one, so at 1.5 the press committed after twenty-seven per cent of the height, and the melt band that turns the dome into paper ran from fourteen to twenty-seven. About ninety pixels of thumb travel for the entire transformation the game is named after.

I had shot that gesture at ten evenly spaced depths all night and read the strip as a globe, then a map. The half-crushed dome from the concept plate, the one image I had written down as the verb, existed for three frames of a swipe and anyone moving at speed never saw it.

The fix is the number matching its own comment. At 1.0 the press commits at exactly forty per cent, and the dome deforms across a fifth of the screen: squat on the copper, continents running off its shoulders and down onto the sheet, which is the picture I had been describing to myself for six hours without checking that it was on screen long enough to see.

Two things bit me tonight and both were the same thing. The needles were off the bottom of the frame and the crush was off the end of the clock. Neither is a bug you can find by reading the code, because in the code both were correct. You find them by holding the phone.

What comes out

Ten customers, all invented, each caring about exactly one needle and none of them about the other two. A shipping line whose captain has never turned a wheel. A relief fund splitting money by square kilometre. A cruise company that would very much like Greenland to stay enormous, which is the one round where the machine wants you to lie on purpose. Round nine asks for honest sizes and honest bearings at the same time and cannot be satisfied, and the punchline says so.

Every round ends with the map getting used, on a lantern-slide card, with the ship walking the bearing the player actually drew. The joke is performed on their sheet, not a stock one.

The last customer is a schoolteacher who wants no gauge at all. Whatever comes out goes on a wall. That one is the only map in the game with no job, which makes it the only one that cannot be got right.

Watch

SOMETHING HAS TO STRETCH · 5:13