Build1 publisher3 min readPublished
Don Watch replays the 2019 Sheffield flood from gauge records inside one browser tab
Don Watch shades terrain and water through three.js TSL and runs a small open model on the visitor's own GPU, while every hydrology number stays in deterministic TypeScript that the model is only allowed to read.
The Engineer · Build desk

What happened
- Don Watch replays the November 2019 Sheffield flood hour by hour from Environment Agency gauge records, and the water level on screen is the level that was recorded at that time.
- Terrain, water and post-processing all run through three.js on its WebGPURenderer, shaded in TSL, Three's node shading language, instead of hand-written GLSL strings.
- The model, an open one in the 1-to-2-billion parameter range, loads into the page through WebLLM and runs on the visitor's own GPU, with no server and no API key.
- The app ships as a Next.js static export and installs as a progressive web app, with a service worker caching the shell, the flood data and the model weights.
- Gauge and rainfall data, flood-warning areas and live sensors come from the Environment Agency, the river and place names from OpenStreetMap, and the terrain from open elevation data.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- capability Once the cache is warm the replay keeps running with the network cut, so only live mode still depends on reaching the Environment Agency feed.
- constraint Fidelity is capped by the gauge network rather than the renderer: central Sheffield offers a handful of river-level readings for the fill to spread from.
- cost Adopters pay in GPU scheduling work before the tab feels responsive, because the render loop and on-device inference compete for the same device.
- decision Teams shipping safety-adjacent AI features now have a worked example where a reviewer can read the thresholds in TypeScript and the model only phrases what the code produced.
The water here is a height field, not a flat plane with a normal map. The simulation reads the gauge height for the current instant, and a breadth-first fill spreads out from the real river vertices through submerged ground only, so an isolated dip away from the channel never fills [6]. That height field is written to a texture, and the shader samples it for both displacement and depth [7].
The shoreline was the expensive bug. The draped water and the terrain are two different resolutions of the same heightfield, so at the waterline they interpenetrate [8]. A depth-buffer bias cannot fix surfaces that actually cross, so the author lifted the water a couple of metres above the ground, which is sub-pixel at the camera distance and invisible [9].
The limit put on the model is the part I would copy. "The important design decision is what the model is not allowed to do," wrote the developer, who posts on dev.to as fortitudeomnis [15]. The same post calls the split "the honest place for a language model in a safety-adjacent tool, and it's why the whole thing is framed as a decision-support projection rather than a flood warning" [17].
Five small agents work the flood while it plays, according to the post: an orchestrator hands out the job, two ingest agents read gauge and rainfall, a forecast agent projects ahead, a risk agent ranks the sites in the water's path, and a comms agent drafts a plain-language brief [13]. That list names six roles [14]. Presumably the orchestrator does not count itself.
WebGPU and a local model in one tab contend for the same GPU, and the author reports that bracketing generation so the render loop can yield "turned out to matter" [22]. Serving a Next static export under a sub-path means every runtime fetch that basePath does not cover needs the prefix, and the service-worker scope moves with it [21].
The question the author set was "how much of a genuinely serious flood tool could I put inside a single browser tab, with no server behind it, and still have it feel like a tool rather than a toy?" [24]. For that answer to transfer, the state has to stay this small: one gauge reading per timestep drives a fill over a fixed mesh [6], and the event's data arrives with the page [12]. A hydraulic solver that needs a cluster still needs a cluster. The inference backend sits behind an interface, with WebLLM as the default and a scripted fallback for devices that cannot run the model at all [11]. The post identifies the model only as an open one in the 1-to-2-billion parameter range and does not report frame rates or a device matrix [23].
What to watch
- Whether the developer publishes frame rates or a tested-device list for WebGPU rendering and WebLLM inference sharing one GPU.
- Whether live mode against the Environment Agency feed holds up during an actual flood, when gauge readings move fastest.
- Whether the scripted fallback gets exercised often enough to show how many visitors' machines cannot load the model at all.