Inspiration
Disaster guidance always ends the same way: "print a copy of important information and keep it somewhere safe." Then the flood, the fire, or the evacuation happens, and paper behaves like paper. A corner tears off and you lose the shelters starting with S. Water hits it and a column of phone numbers goes.
That failure is structural, not accidental. A printed list is positional: every fact lives at one place on the page, so destroying that place destroys the fact. We wanted a sheet where that is not true — where no part of the page is more important than any other part, and where losing half of it loses nothing at all.
What it does
PAPERFOUNTAIN prints a resource directory as a fountain-coded sheet, then lets you destroy it and watch the directory survive.
The page carries 320 emergency and community resource records — shelters, food banks, cooling and warming centers, legal aid, free clinics, water points, charging stations — with names, addresses, phones, hours, capacity and access flags. Instead of printing them as a list, it packs them to 8,924 bytes and spreads that payload across 1,152 printed data cells using a Luby Transform fountain code.
Then you attack it. Tear a corner, spill coffee, burn an edge, fold it, run it through a bad photocopier. A live scanner reads the surviving cells straight off the pixels, a peeling decoder rebuilds the payload, and the directory on the right stays searchable — until you go far enough that it doesn't, and the app tells you exactly how far past the edge you went.
How we built it
Everything is written from scratch — no codec library, no chart library, no CDN.
Packing. A custom bit-level format (magic FPK1) with a shared word dictionary, a 64-symbol alphabet, varint fields, and bit-packed enums for city, category, ZIP, phone and hours. 320 records in 8,924 bytes is about 28 bytes per record.
The fountain code. A Luby Transform encoder with a proper Robust Soliton distribution (c=0.03, delta=0.05) over k=372 source blocks of 24 bytes. Each droplet is 32 bytes: a 2-byte seed, a 2-byte degree, a 24-byte XOR of the selected source blocks, and a CRC-32. The neighbour set is derived from the seed, so a decoder needs nothing but the seed and k — there is no index to lose.
The sheet. A real Letter page at 216 dpi. Each droplet is drawn as a 16 x 16 module block (256 bits = 32 bytes), module size 0.35 mm, which a 600 dpi laser prints as an 8 x 8 dot block. It has a print stylesheet and genuinely prints.
The damage is real. This is the part we care most about. The damage tools do not draw an overlay and mark cells dead. They paint on the same canvas the scanner reads — destination-out for a tear, a real gaussian blur plus staining for a soak, a ragged char boundary for a burn, contrast collapse and toner speckle for a photocopy. The scanner then re-reads modules from pixels, rebuilds each 32-byte cell, and the CRC-32 is what decides whether a droplet lives. A corrupted cell is dropped, not trusted.
Decoding. A belief-propagation peeling decoder over whatever survived. The reconstructed directory, including its search index, is built from the DECODED bytes — never from the source JSON. The UI says how many records came off how many surviving cells so that provenance is explicit.
The statistics are measured, not modelled. P(decode) at a given damage level comes from a Monte-Carlo harness in a Web Worker that runs real repeated decodes over random cell subsets. The overhead figure comes from a binary search for the minimum number of droplets that actually decodes.
Challenges we ran into
Making damage physical rather than symbolic. It is easy to write cell.alive = false. Doing it honestly meant the scanner had to composite torn-away paper against a white backing, threshold real luminance, and let the CRC arbitrate — which is also what makes the burn edges and coffee rings destroy a believable, ragged set of cells instead of a neat rectangle.
Tuning the Robust Soliton parameters was the other one. Too few degree-one droplets and the decode never starts; too many and you waste page area on droplets that carry one block each.
Accomplishments we're proud of
Measured live in the demo, every number computed at runtime:
- 42.9% of the page destroyed — 658 of 1,152 cells surviving — and all 372 of 372 source blocks peel. The decode is byte-exact (FNV-1a hash of the recovered payload matches the original).
- 100% decode success up to 50% damage, over 60 random Monte-Carlo trials per level.
- The cliff is at 65% damage: 98.3% success at 55%, 78.3% at 60%, 0% at 65%.
- Measured coding overhead: median 447 droplets for k=372, about 1.20x, with a best case of 1.13x.
- When it does fail, it reports how many more surviving cells it would have needed, measured by adding destroyed cells back until it decodes.
Honest limits
The scanner reads the sheet's own canvas, not a photograph of a printed page. Real capture adds perspective, lens blur, uneven lighting and halftoning, and would need fiducials and deskewing before this became a phone-camera product. That is real work and we have not done it.
The directory is synthetic, generated deterministically from a seed. The record schema is realistic but the facilities are not real, so nobody should print this sheet and drive somewhere.
The cliff is sharp, which is inherent to LT codes rather than a bug — but it means "survives 50% damage" and "survives 65% damage" are very different claims, and only the first is true here.
What we learned
Erasure coding is normally invisible infrastructure. Putting it on paper makes it legible: you can watch the redundancy get consumed, see the peeling stall, and understand the cliff without knowing what a Soliton distribution is. The demo turned out to be the best explanation of the code we could have written.
What's next
Phone-camera capture with fiducial markers and deskewing, multi-sheet payloads for larger directories, a RaptorQ-style precode to soften the cliff, and a print-at-home generator that takes any structured dataset.
Built With
- canvas
- crc32
- disaster-relief
- erasure-coding
- fountain-codes
- information-theory
- javascript
- luby-transform
- offline-first
- web-workers
Log in or sign up for Devpost to join the conversation.