Nine shards across the middle of the screen, four of them holding the payload and five holding parity. Click any of them to kill it. While four are still standing — any four, in any combination — the bytes under the dead ones come back exactly, and the strip underneath reads the payload out in full. Kill one more and the message does not degrade. It develops regular holes, and the readout stops counting shards and starts counting candidate payloads.
The piece is two drawings of one sentence.
The shape, on top, is over the reals, because that is the only field a polynomial has a picture in. A degree k−1 curve, sampled at n positions. Any k of those samples pin the curve — so when a sample dies, the curve re-derives from the survivors and lands on the dead position at exactly the value that was there. That is what the green ✕ means: not approximately recovered, recovered. Take one more away and the fan opens: nine curves, each of them a genuine degree k−1 polynomial through every surviving point, each disagreeing wildly about the ones you lost. Nothing in the picture tells you which is yours, because nothing in the data does either.
The bytes, below it, run the identical algebra over GF(2⁸), which is what a
real implementation uses and the reason a shard of a 4 KB block is 4 KB rather
than some widened float. Addition is XOR, so there is no minus sign in
interpolateAt — (x - xm) is written (x ^ xm) and that is the whole
adaptation. The encode is systematic by construction: the payload bytes are
the polynomial’s values at x = 0…k−1, so shard i is byte i for the first k
shards and nothing has to be copied to make that true.
The three readings
Any k. The usual mental model of parity is RAID-5’s: one extra disk, the XOR of the others, and it rescues exactly one failure. That intuition does not survive this demo, because P0 is not more important than P3 and D1 is not more important than P1. Kill all four payload shards and leave five parity shards — the button marked down to k will often do exactly that — and the payload still comes back whole. There is no distinguished shard. There are only k equations and k unknowns.
One too few is not nearly enough. Hit one too few and look at the payload strip: the shards still alive hand you their own bytes and strictly nothing else, so you get every fourth character and a run of dots. The readout says 10^21, which is 256 raised to the free bytes left, and it is not a difficulty estimate. Every one of those payloads is consistent with every byte you still hold. There is no computation that distinguishes them, so the usual intuition — lost, but recoverable with effort — has no referent here. This is the cliff that makes erasure coding worth operational care: durability is a step function, and the step is one shard wide.
What it costs. The two bars at the bottom put the scheme against the thing it replaces. To survive five simultaneous losses by replication you keep six copies, which is 6.00× your bytes. To survive five losses with a 9/4 code you keep 2.25×. Drag n up and the gap widens, because replication’s overhead is f + 1 and the code’s is (k + f)/k — the same f, divided by k. That division is why every large object store eventually stops keeping copies, and it is also why the thing is less forgiving: a replicated object with one copy left is fine, and a coded object with k−1 shards left is gone.
Controls
k and n set the code; n is held above k. lose one kills a random
survivor, down to k kills until exactly k remain, one too few goes one
past that, repair all restores everything, and new curve rerolls the
shape. Clicking a shard column toggles it directly. Left running, it fails
shards one at a time until it falls off the cliff, holds there, and repairs.
Reuse
src/through.js is a framework-free ES module with no dependencies:
gfMul(a, b)/gfDiv(a, b)— GF(2⁸) with the usual 0x11d polynomial, via log/exp tables built at load.interpolateAt(points, x)— Lagrange in GF(2⁸); the one function the whole scheme is made of.encodeStripe(data, n)/recoverStripe(alive, k)— systematic encode of k bytes to n, and rebuild from any k{x, y}survivors.encodeMessage(text, k, n)/recoverMessage(enc, aliveFlags)— the same over a whole payload, striped k bytes at a time.recoverMessagereturns aknown[]alongside the bytes so partial knowledge is representable rather than silently zero-filled.candidateCount(k, alive, stripes)— how many payloads fit the survivors, as a BigInt, withformatCountfor display.makeCurve(k, n, seed)andfanCurves(...)— the real-valued picture and the family of curves still consistent with a short read.overhead(k, n)— the comparison against replication at equal durability.
The GF layer is small enough to lift whole into anything that needs erasure coding or Shamir secret sharing, which is the same mathematics with the threshold read the other way round: there, the fact that k−1 shares tell you exactly nothing is the feature rather than the cliff.
demo/ carries its own copy of the module (ADR-0002) and scripts/screenshot-demo.mjs
regenerates thumb.png and the two stills in media/.
