I Rebuilt a Solar Eclipse in WebGL, No Photographs
- Rebuilt the 12 August eclipse as a WebGL page with no photographs, then added the 2027 and 2028 eclipses to the same engine
- Berlin saw 84.4 percent covered while Burgos went total, and the parallax correction is the only reason those differ
- Drew a full corona on an 84 percent partial because I was wrong by four orders of magnitude about coronal brightness
- Built a 200 frame automated sweep that caught a dark ring, a corona seam, and three of my own bad assumptions
- Obscuration validates to within half a percent, and the two places the model cannot settle are labelled uncertain on the page instead of hidden
On 12 August a solar eclipse crossed Europe. Berlin got 84 percent of the sun covered at ten past eight in the evening. Northern Spain, at almost the same minute, went completely dark. I spent two afternoons rebuilding it as a website you can scroll through, and the interesting part was not the graphics. It was how many confident, wrong assumptions I had to be argued out of by measurement.
The disc is computed, not photographed
The page draws the sun in a WebGL fragment shader. No images, no video, no textures. The photosphere is limb darkened using a two term law, so the edge of the disc dims because of geometry rather than because someone painted a gradient. Granulation, the convection cells that give the surface its texture, is evaluated on a sphere rather than on a flat circle, so features foreshorten as they turn over the edge.
The moon is the part that surprised people. Its edge is deliberately ragged, built from noise rather than drawn as a circle. That single decision is why the last sliver of sunlight breaks into beads instead of switching off cleanly. Baily's beads are not drawn anywhere in the code. They fall out of the geometry, which is exactly how they work in the sky: sunlight streaming through valleys on the lunar limb.
One honest footnote. Nothing photographic is sampled at runtime, but the colour of the disc was calibrated by measuring photographs of this exact eclipse, then solving for the anchors rather than eyeballing them. Cream near white with a high sun, gold through the descent, rose only at the very bottom. The measurement is where the realism comes from.
There is a cost to this. A photograph of an eclipse is free and perfect. A computed one takes days and is wrong in a hundred ways until it is not. The payoff only arrives at the end: a photograph shows you one eclipse from one place, while a model shows you any eclipse from anywhere, including places nobody stood with a camera.
That approach has one practical consequence worth stealing. When the visual is a consequence of a model rather than a drawing, you cannot fake a fix. If something looks wrong, the model is wrong, and you have to find out how. That is slower and much better. If you want the same idea applied to interface work, The Micro-Interactions I Add Last That Make a UI Feel Fast covers the smaller scale version.
Why Berlin and Burgos saw different skies
Every location on the page comes from a solar and lunar ephemeris, with one step that does all the work: the topocentric parallax correction. The moon sits about a degree away from where a calculation centred on the Earth would put it, and a degree is wider than the moon itself. Skip that correction and every city on Earth sees an identical eclipse, which is nonsense.
With it, the numbers separate properly. Berlin reached 84.4 percent obscured with the sun 3.1 degrees above the horizon. London reached 90.7 percent. Burgos went total. Those are not decorative differences, they are the entire reason people travel for eclipses.
The local clock turned out to be a useful cross check on the geometry. Berlin's maximum fell at 20:10 in the evening with the sun barely three degrees up. Burgos went total at 20:31 with the sun at eight degrees. Palma is the clearest test of the two agreeing: the page puts its maximum at 20:33 with the sun 2.2 degrees above the horizon, and local sunset that evening was around 20:40. The clock and the geometry are computed separately, so when they land in the same minute, each one is evidence for the other.
I validated the output against published figures before shipping any of it, and obscuration lands within half a percent: London 91, Paris 92, Berlin 84. It is also wrong in a specific, documented way, and this is the part I would rather state myself than have someone find. The lunar theory is truncated. That is good to tens of arcseconds, which is ample to tell a deep partial from a total nearly everywhere, and not ample within about a percent of the umbral edge. Two places land exactly in that band: Ittoqqortoormiit at 98.9 percent and Reykjavik at 99.6.
I could not settle those from my own maths. It cannot be determined from here whether the theory is running short or those towns genuinely sat just outside the shadow. So Reykjavik is corrected against the published record rather than trusted, and both are flagged as uncertain in the page itself. Publishing the limit costs nothing, and it is the only reason the numbers next to it are worth anything.
The corona mistake, wrong by four orders of magnitude
Here is the error I am least proud of and learned the most from. Early on, Berlin at 84 percent covered rendered a full streamer corona. It looked spectacular. It is also impossible.
The corona is roughly 100,000 times fainter than the surface of the sun. At 99 percent eclipsed, the remaining one percent of the disc is still around a thousand times brighter than the corona. Any visible sliver of sun drowns it completely. That is the reason people cross oceans for the two minutes of totality rather than watching a 95 percent partial from home: the difference is not a matter of degree, it is a different phenomenon.
My code was gating the corona on a term that ignored one geometric variable, so a city the shadow missed entirely reported the sun and moon as perfectly aligned. The fix was small. Believing the fix was needed took a published brightness ratio, because the wrong version looked better.
There is a second glow that a deep partial genuinely has, and I had left it out: forward scattered light hugging the bright crescent. That one is real, it is in every photograph of a deep partial, and it is what makes those frames beautiful without lying about them.
The sweep that argues back
Two afternoons of visual work produced a specific problem: I could not tell whether a change fixed one frame and broke another. So the project grew an automated sweep. It walks all eight locations through 25 positions each, 200 frames, hides the interface, and checks the pixels.
It looks for blank frames, hard horizontal seams, coronas on partial eclipses, coronas missing at totality, dark rings around the disc, and captions that contradict their own obscuration number. Every one of those checks exists because that exact bug shipped once.
The sweep caught three things I would have missed. A dark ring around the sun, where the limb rendered darker than the sky beside it, which no real sun does. A hard seam running left from the disc centre, caused by feeding an angle into noise as a linear coordinate, so plus and minus pi sampled unrelated values. And a site rendering nearly black at its maximum because one curve assumed twilight arrives at high obscuration, when in truth it needs the umbra.
The sweep also lied to me twice, which is its own lesson. Once it reported the corona missing at totality when the render was fine, because my capture hid the interface in a way that collapsed the layout and shrank the disc while the analyser measured the old size. If a test and the thing it tests can drift apart, they will. The page now publishes its own geometry and the analyser reads it, so there is one copy of the truth. For the general pattern, Accessible Color Contrast Without Killing the Brand makes the same argument about measuring instead of eyeballing.
Three eclipses for the price of one engine
Once the geometry came from an ephemeris rather than from hand tuned constants, adding eclipses became data entry. The page now holds three: 12 August 2026, the total that just happened. 2 August 2027, six minutes and 23 seconds of totality over Luxor, the longest on accessible land until 2114. And 26 January 2028, an annular eclipse that reaches Andalusia with the sun under seven degrees up, a ring of fire low in the evening.
The annular one needed zero new rendering code. When the moon is smaller than the sun in the sky, the ring emerges from the same equations that cut every crescent, the corona stays correctly invisible, and the sky never falls to twilight. All of that came free from gates written days earlier for different reasons.
It self checked, too. For an annular eclipse the obscuration must equal the size ratio squared, and every in band site does exactly that against its own ratio: Sevilla reads 0.8237 for a ratio of 0.90758, Puerto Ayora 0.8244 for 0.90799. Nobody typed those obscurations in, they fall out of the same geometry, and they land on the square every time. When your model produces a number you did not put in and it matches theory, that is the good kind of surprise.
What the interface had to give up
Two afternoons of building produced a dashboard with more instruments than screen. On a phone it covered the eclipse: the readouts took two thirds of the frame, and the thing people came for was the part you could not see. Every fix was a subtraction. The telemetry moved behind a sheet you open on request, and the site list became a grid where all eight places fit at once instead of a row hiding six behind a swipe nobody knew about.
The corona forced one more. On a phone the ring reaches wider than the screen, so it read as a glow spilling off both edges. A desktop fits a large disc and a whole corona. A 390 pixel phone does not, so the disc shrinks to let the ring close.
Bottom Line
The lesson that transfers has nothing to do with astronomy. Every serious bug in this project was a confident assumption that survived until something measured it. The corona looked right and was wrong by a factor of 100,000. The sky looked deliberate and was rendering night at midday. The test suite passed while measuring the wrong pixels.
Taste catches what tests cannot, and tests catch what taste will not. Building with AI makes both cheaper, and it makes the third thing, deciding what is actually true, the entire job. I did not write the shader code. I did decide, repeatedly, that what it produced was not yet honest.
The page is free and takes about a minute: scroll-eclipse.vercel.app. It will also work out what the eclipse looked like from your own coordinates, computed in your browser, with nothing uploaded anywhere. If you want more of how I work with Claude on things like this, the Claude Blueprint is the full setup.
Back to all articles