I used to do both jobs in the production engine, and every tweak cost a full Python render. That engine draws the journey art for Habitagram, a habit tracker due December 2026: a climb in six stages the code calls biomes. This entry follows one of them, the sea, from browser sliders to the shipped engine.
The split is simple. Prototype in a browser canvas, lock the parameters, port the math 1:1 into the Python engine. A shared spec holds both sides to the same numbers, so “1:1” is something a test checks.
The harness: one HTML page, no dependencies
The look-dev harness (the page where I tune the look) is a plain canvas page plus four ES modules. No framework, no build step, no npm install. For the sea biome it exposes what the look needs. Five color pickers (sand, sky reflection, shallow water, deep water, desert body). A seam editor for the edge where sand meets water, with a position slider and three edge kinds (catmull, torn, hard). Sliders for the ripple texture’s frequency, amplitude and strength. Path waypoints you move by clicking the canvas. Every control redraws immediately.
When the look lands, one button validates the parameters and downloads a JSON file. Another takes a screenshot. There is also a headless variant: a Node script that runs the same renderer module and writes a PNG using nothing but Node builtins, so I can eyeball a candidate without opening a browser.
The point of zero dependencies is speed of iteration on the harness itself. Adding a slider is four lines.
The contract: one spec, two implementations, one fixture
The dangerous step is the port. JS and Python both have floating point, but they don’t share your intentions: which Catmull-Rom parameterization (the curve through the seam’s control points), how gradient stops interpolate, what happens outside the domain. A visually identical port that quietly disagrees at the edges will bite months later.
So the portable math lives in a spec with four functions: Catmull-Rom evaluation, multi-stop gradient interpolation, band-local position remapping, and a contour-line field (the sine ripple on the water). Both stacks implement it, and both are asserted against the same committed conformance fixture, a JSON file of inputs and expected outputs at 1e-6 tolerance. If the JS harness and the Python engine ever disagree about the curve’s value at x = 0.375, a test fails before an eyeball has to notice.
This is the Catmull-Rom block of the fixture, values as committed, whitespace condensed:
"catmull_rom": {
"control_points": [[0.0, 0.0], [0.25, 100.0], [0.5, 50.0], [0.75, 150.0], [1.0, 100.0]],
"queries": [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0],
"expected_y": [0.0, 53.125, 100.0, 75.0, 50.0, 100.0, 150.0, 131.25, 100.0],
"tolerance": 1e-06
}
Some things stay out of the contract on purpose. The paper grain and torn-edge texture are Python-only, built on numpy’s RNG and scipy’s gaussian filters. The browser only approximates them, and no test pretends otherwise. Grain drift is caught downstream by the engine’s golden fingerprint instead, a sha256 of the raw render pinned in a test.
The port, concretely
The locked sea look exported as a JSON of parameters, and the port is those values baked into the engine’s biome spec. The seam became a Catmull-Rom edge at 0.26 from the top with control points at (0.0, 0.24), (0.5, 0.29), (1.0, 0.25). The water became a three-stop gradient from sky reflection through shallow to deep. The ripple became a contour signature at frequency 5.5, gated to render only below the seam so the sand shore stays smooth.
That gating didn’t exist in the engine, so the port added it as a capability with its own test: sand rows byte-identical to before, water rows visibly modulated. Each port leaves a feature behind.
The last step is the sign-off. The engine renders at the pinned seed (42), I compare against the prototype on a phone, and only then does the golden fingerprint get re-pinned. Until that eyeball happens the golden stays red on purpose.
Why this beats picking one tool
JS finds the look and Python ships it. The fixture is what lets the two disagree loudly instead of silently. I get instant redraw while exploring and full determinism while shipping, for the price of one spec file and a fixture I wrote once.
The other half of this contract, the golden that catches what the fixture cannot, has its own entry: golden fingerprints for generative art.
