Effects and simulations

How water, weather, fire and falling sand fit a palette-indexed engine on a phone: most effects bake once and blit every frame, and the ones that must change every pixel every frame are tiny integer automata over typed arrays, uploaded a row at a time. Effects that obey the palette are cheap; effects that ignore it need shaders. Tycho already ships a light map drawn by light.Smooth, post.Blur for bloom and wet reflections, deterministic particles in Tycho.fx, and drawPixels for buffers rewritten each frame (the renderer). What’s missing is the CPU side: weather, small automata and a falling-sand sim. Light has its own page.

Proposed: none of this is in Tycho yet. The demos run here in the page, written in plain JS with the study’s own kit (demos/kit.js), not Tycho; each says how it would run in Tycho: per frame (the GPU stand-in’s calls), pre-bake (painted once offscreen), or shader (per-pixel work every frame). What would move into Tycho, and when, is in the roadmap.

Water and weather

At pixel resolution water reads through a few cheap tricks: a vertically flipped copy of what’s above the shoreline, a per-row horizontal sine offset for ripples, cycling highlight inks for glints and foam, and a scrolling strip for waterfalls. All but per-pixel displacement are drawImage strips: one strip per row, so 50 rows of water is 50 quads (inference). Tycho’s post.Blur already reflects “wet strips” with a ripple (the renderer). Mark Ferrari animated waves, streams and waterfalls purely by palette cycling over one image (Effect Games); that route is on motion.

Reflections. The scene mirrored into water with a per-row sine offset, foam at the shore and a few glints. Per-row drawImage strips fit the stand-in; post.Blur already has wet strips.

Nearly all weather is either particles (rain, snow, motes, fireflies, leaves) or scrolling, pre-painted layers (fog, clouds, cloud shadows, god rays) drawn with alpha, multiply or lighter; heat haze is the water’s per-row offset at 1 pixel (inference). A particle whose position is a pure function of its index and the tick needs no state, so a frame draws the same however often it’s drawn, which is how the White Dragon’s sparks already work in Tycho.fx (effects). Dithered fog keeps the palette exact; smooth alpha fog doesn’t (inference).

Weather. Rain with splashes, snow, dithered fog and blinking fireflies. Each particle's position is a pure function of its index and the tick, so a frame draws the same however often it is drawn.

Small automata

The simulation effects that matter are tiny integer automata.

At 320×180 or smaller regions, each of these runs in a typed array and uploads changed rows through the stand-in’s drawPixels (inference). The fallback rule is to never call getImageData on a GPU canvas, since readback is the main phone hazard (WebKit bug 235196). The fire and the sand below carry the shader tag because they are per-pixel work every frame; at these sizes that work fits a CPU buffer uploaded with drawPixels.

Doom fire. Every cell copies the one below minus 0 or 1, shifted sideways at random (Fabien Sanglard's write-up of the PSX version). The output is already palette indexes; a seeded source keeps it deterministic.

Falling sand: the materials

Every falling-sand game sorts materials into a few movement classes (static, powder, liquid, gas, plus fire-like cells), then separates them with a handful of numbers.

A demo’s materials, taken from those sources:

Material Class Weight Key behavior and reactions Basis
Sand powder 90 melts to glass via lava; water makes wet sand TPT, Sandboxels
Water liquid 30 boils at 100 °C, freezes at 0 °C; salt makes salt water (1/50); kills fire TPT, Sandboxels
Salt water liquid 35 freezes lower (−21 °C in TPT, −2 °C in Sandboxels); boils into steam and salt TPT, Sandboxels
Oil liquid 20 floats on water; about 25% sideways chance; ignites at 2% TPT, Sandboxels
Lava thick liquid 45 10% sideways chance; water makes stone and steam; remembers what melted TPT, Sandspiel
Acid liquid 10 dissolves non-glass at Hardness/1000; about 25 kills, then dies TPT
Glass static 100 acid-proof (Hardness 0) TPT
Wood static 100 burns 90 to 300 ticks into ember, charcoal or ash Sandspiel, Sandboxels
Plant static, grows 100 drinks adjacent water (1/50 TPT, about 19% Sandspiel) TPT, Sandspiel
Ice static 100 spreads into water (7%); fire or lava melts it Sandspiel
Steam gas 1 condenses below about 95 °C; clouds near the ceiling TPT, Sandboxels
Fire short-lived gas 2 life 180 to 259 ticks; ignites by flammability; becomes smoke TPT, Sandboxels
Smoke dissipating gas 1 deletes itself 5% per tick Sandboxels
Gunpowder powder 85 ignites at 60% (TPT) and explodes TPT, Sandboxels

Falling sand: the reactions

Reactions come with real per-tick chances, which set the pace of the toy.

Reaction Chance Source
Lava next to water becomes stone 100% Sandspiel
Salt dissolves into water 1/50 per tick TPT WATR.cpp
A flammable neighbor ignites (Flammable + pressure·10)/1000: 2% for oil and wood, 60% for gunpowder, 100% for nitro TPT FIRE.cpp
Water kills fire; water evaporates Always; 1/30 TPT
A plant drinks neighboring water 1/50 TPT PLNT.cpp
Acid dissolves a neighbor (TPT) Hardness/1000; dies after about 25 kills (life 75, stopping at 50) TPT ACID.cpp
Acid eats a neighbor (Sandboxels) 5% per side, 10% below; ignores glass and the other waters Sandboxels
Ice spreads through water About 7% per tick Sandspiel
Two steam cells near the top of the screen form a cloud 5% Sandboxels

Acid is bounded in both so it cannot erase the world. Three reaction kinds cover a demo: contact reactions (A, B) → (A′, B′) at chance p, temperature transitions, and burning. A 28-material table fits in a dense 784-entry array of results and chances stored as bytes out of 256. Chances taken from 60 Hz sources must double if the sim runs at 30 Hz (inference).

Falling sand: the implementation

The implementation is well settled.

No source measures plain JavaScript sand on phones. The best anchor is Sandspiel’s 300×300 grid in Rust compiled to WebAssembly, which hits 60 fps on many devices but not all, so its author regrets spending headroom on a bigger canvas (Making Sandspiel). Elsewhere’s 320×180 is 64% of that cell count, so a Tycho sim needs typed arrays with a one-cell wall gutter, property tables indexed by material, sleeping chunks, an optional 30 Hz tick, and a seeded xorshift in place of Math.random. The last of these lets the bots and golden tests replay a sand quest exactly. Each material’s grain should pick among two or three of the frame’s inks rather than free RGB (inference).

The design lesson is to keep it legible. Sandspiel’s single-tap tools and Noita’s finding that players never noticed slow moss both argue for a few strong chains in one constrained toy with a goal, such as melting sand into a glass bridge with lava (Making Sandspiel; 80.lv).

Falling sand, 26 materials. Every material has a weight, a viscosity, a flammability and a heat conductivity, and every cell carries a temperature. Water freezes and boils, and steam rains back. Lava turns to stone in the lake and sand to glass. Acid eats everything but glass and wears out. Salt water melts ice, gunpowder and gas explode, and seeds grow toward water. The numbers follow the source code of The Powder Toy, Sandboxels and Sandspiel. Sideways flow is a chance of 100/viscosity0.25%; acid dies after about 25 cells. It runs 160×120 in 2 to 6 ms a frame, with sleeping 16×16 chunks and one drawPixels upload. Pick a scene, or choose a material and pour; Heat view shows the temperatures.

Where each effect runs

Under Tycho’s rules, “stand-in” means per-frame drawing with fillRect, drawImage, alpha, the four blends (source-over, lighter, destination-out, multiply), transforms and gradients; “pre-bake” means painted once offscreen, where anything goes; “CPU buffer” means a typed array uploaded each frame with drawPixels; “shader” means gpu.program or gpu.lightPass. The placements are inferences from the renderer’s documented limits (the renderer) and the effects research. Light’s rows are on its page.

Technique family Where it runs How
Palette cycling Pre-bake (CPU buffer for large areas) K phase canvases, or changed rows via drawPixels
Particles, rain, snow, motes, fireflies Stand-in fillRects at rounded positions
Fog, cloud shadows, god rays, caustics Pre-bake, then stand-in Scroll baked textures with alpha, multiply or lighter
Ripples, heat haze, reflections of painted layers Stand-in One drawImage per row; post.Blur wet strips already exist
Doom fire, falling sand, Stam fluid, Life CPU buffer drawPixels of changed rows
Mosaic and iris transitions, screen shake, hit stop Stand-in Two blits; baked masks; camera offset
CRT curvature, chromatic aberration, sharp bilinear, color LUTs on RGB Shader Against the eight-ink look by design

Two phone hazards matter most: reading pixels back from a GPU canvas with getImageData, and running effects after upscaling, which touches about 50 times the pixels of 320×180. Do every effect at internal resolution and upscale last.

Sources

The full research is in the notes on effects and rendering and the notes on falling-sand materials.