The Mills · Demos

devils-dust

Wool in the air of a shoddy mill — mills/devils-dust

Devil</header>#39;s Dust: wool wisps flocking above the devil</header>#39;s drum, tuning panel open

Summary

devils-dust opens a 1280×720 scribbler window on the inside of a mill. A hundred wisps of wool flock by the three classic boids rules — a famous 1987 recipe for lifelike flocking, described below — above the devil, the toothed drum that tore rags back into fibre. The drum throws them, loses them to gravity, and takes them back off the floor. A tabbed tuning console lets you reshape the storm while it blows: twenty-one sliders on four tabs, a five-position speed lever (SLOW/NORMAL/MAX/ELEVEN and a red STOP bar), live meters on the floor, and a machine-room drone from the buzzer that rises and falls with the drum. Everything answers to the mouse or the keyboard; B mutes, H hides the panel, Q quits.

The Original

The flocking model and the term "boids" are Craig W. Reynolds's (red3d.com/cwr/boids, and see the Wikipedia article); this mill is an original implementation of his published model. See THIRD-PARTY-NOTICES.md.

Two originals, sixty years apart. The flocking is Craig W. Reynolds's boids — "Flocks, Herds, and Schools: A Distributed Behavioral Model," Computer Graphics 21(4), from SIGGRAPH '87, the annual computer-graphics conference. That paper demonstrated that lifelike group motion needs no choreographer, only three local rules: separation, alignment, cohesion. Every murmuration — the swirling cloud a starling flock makes — in every film title sequence descends from it. This mill keeps the three rules textbook. It adds one steering force Reynolds's model explicitly allows: the draft rising off the devil.

The scene itself is older. Devil's dust was the airborne fibre that filled a West Riding shoddy mill. The devil threw it off: the spiked drum that tore woollen rags back into spinnable fibre, the machine the whole industry (and this language's name) was built around. The demo is the language's origin story, animated: mixed rag-stock colours in the wool, a drum that spins up with iron inertia, and dust that settles where it falls until the sweep returns it to the machine. Even the parser agrees — the tokenizer, the part of the compiler that tears Shoddy source apart into words, lives in src/Shoddy.Devil/. As for the lever position past MAX: Spinal Tap — the mock-rock film whose amplifiers famously go up to eleven — owns the rest of that sentence.

Running It

bin/mill run mills/devils-dust/devils-dust.shoddy

or, from mills/devils-dust/, use the wrapper — ./build.sh, or ./build.ps1 on Windows:

./build.sh          # run the demo (also: ./build.sh run)
./build.sh test     # run the headless model checks

Like every scribbler program it must run under mill run. A woven dotnet FILE.dll has no window backend — nothing to open a window with. The folder also carries sound-lab.shoddy, the workbench the drone was designed in: no wool, just instruments — nine square/triangle/sine recipes with live keys for pitch, detune, gain, and churn. Run it the same way when you want to audition a sound before wiring it in.

It also ships on the Shoddy Reckoner's games shelf — scribbler and buzzer together. The drawing is presented as a page, and the drone is rendered by the app's platform audio sink, the part of the app that hands sound to the operating system. The woven program is the same one, unmodified.

The Console

Every behavioural constant — twenty-one of them — lives in a Knobs record carried inside the simulation state, and the panel is a pure view of it: four tabs (LIFE, TEMPO, FLOCK, PACE) of sliders. Each slider is drawn from a knob table in the core that knows every knob's name, rails (its legal limits), and accessors (the words that read and write it). Tabs and knobs are sum types (Type Tab, Type Knob) — types whose value is always one of a fixed set of named cases. So every dispatch is a Select Case over names, not magic numbers.

Controls set a goal; the machine winds toward it. An 1875 devil is tons of iron on a belt drive, and it does not jump to a new speed. The slider handle is your lever and moves the instant you set it; the fill and the value are the machine catching up. The lever presets are just parked goals. SLOW winds the motion down to about a third, with the wool's buoyancy kept — slow wool is still flying wool. NORMAL is the defaults. MAX is every knob at its rail. And ELEVEN is every knob a quarter past its rail, through the raw write the sliders themselves are never given. The STOP bar kills most of the drive in the instant of the pull and lets the rest coast down; the wool rains out and lies where it lands.

One colour language runs the whole console: brick red at the bottom, mute yellow low, green at normal, ivory at the legal top, ember past it. Sliders wear the heat of their value, the lever buttons wear their liveries, and the floor meters each wear the heat of their own reading. A red ALOFT is an empty sky; an ember SPEED is eleven's doing.

The twenty-one sliders, by tab (defaults are the NORMAL lever position; every rail is reachable by hand, and only ELEVEN goes past them):

SliderTabDefaultWhat it turns
LOFT MIN / LOFT MAXLIFE240 / 480Each wisp's buoyancy budget, in frames — how long fresh wool rides the flock before it tires and rains. Lower for a rain-dominated sky, higher for longer aerial displays.
GRAVITYLIFE0.035The settle — how insistently everything tends floorward. The weight of the air.
LIFTLIFE0.1The devil's updraft strength — how hard the drum's draft drives wool upward.
DRAFT RLIFE260The updraft's reach in pixels — how far from the drum the rising air extends.
MIN SPD / MAX SPDTEMPO0.7 / 3.1The speed envelope every wisp lives inside — wool never stalls dead, never streaks like a bullet.
FALL MAXTEMPO1.3Terminal velocity — how lazily raining wool floats down.
FLUTTERTEMPO0.12Constant turbulence — the wobble that makes it wool rather than birds.
TUMBLE P / TUMBLE KTEMPO0.04 / 0.9How often a wisp catches a real tumble, and how hard the kick bends its flight line.
ALIGNFLOCK0.025Reynolds's alignment — matching neighbours' headings. The most "bird" knob; zero it for pure drifting chaos.
COHEREFLOCK0.003Reynolds's cohesion — drift toward the local centre of mass. Clumping.
SEPARATE / SEP RFLOCK0.06 / 16Reynolds's separation — personal space: push strength and the radius it defends.
NEIGHBORFLOCK55Who counts as a neighbour, in pixels — bigger makes one big flock, smaller makes local drifts.
SWEEPPACE0.02Per-frame chance landed wool is swept back to the devil — lower and the floor gathers fluff.
RECYCLEPACE0.012Per-frame chance wool deep in a side exit zone returns early — spreads respawns into a steady trickle off the drum.
WISPS / MOTESPACE100 / 180The live populations — wool and fine dust. Resized on the fly: a deficit sprays from the drum, a surplus is trimmed.
SPINPACE0.264Drum speed, in radians per frame (a full turn is about 6.28 radians) — the machine's tempo, and what the drone's pitch and the churn's rate follow.

And the footer meters, each computed fresh every frame from the same values the renderer draws:

MeterWhat it reads
FPSMeasured frame rate (frames per second), smoothed — green at a healthy 30, sagging yellow when a heavy panel setting (many wisps, wide NEIGHBOR) starts to drag.
ALOFT / RAIN / DOWNThe lifecycle census: wisps riding the flock, raining with spent loft, and resting on the floor awaiting the sweep. The LIFE tab made visible.
ORDERThe flocking order parameter — how nearly the airborne wool moves as one: the size of the average velocity, divided by the average speed. 1.0 is a marching flock (starlings); near 0 is incoherent drift (wool). Watch it move as you drag ALIGN.
SPEEDMean airborne speed — and the one meter that can burn ember, since only eleven pushes it past the MAX SPD rail.
ALTMean height of airborne wool above the floor, in pixels — how high the storm is riding.
THRUWool through the devil per second, smoothed — the speed of the mill's cycle: throw, flight, rain, sweep, throw again.

The Voice

The drone reads the live values, not the goal. So the machine's inertia is audible for free. Pull the lever to ELEVEN and the pitch climbs over the seconds the belt takes to load. Hit STOP and the hum glides down and dies as the drum coasts to rest. The bed is a low sine tone (a sine wave is the smoothest tone there is) with a twin tuned slightly apart. Two close pitches beat — pulse in loudness at the rate of their frequency difference — and that beat is the machine's throb. It runs deep in the volume, and it quickens with speed, because a beat is the frequency difference. Under it sit a triangle wave a musical fifth above, a shimmer wash, and a breath of churn at the drum's own rate.

Past the rails the chord sours by degrees. A continuous heat measure bends the overload voice from a clean octave toward a flat ninth — from a sweet interval toward a sour one — then swells it and crossfades it (blends it gradually) from triangle to square. The buzz arrives and leaves with the ember on the gauges, never as a cliff. Lever pulls speak short low phrases; eleven's riff climbs by semitones, of course. Wool landing on the floor pats quietly, and B silences the lot. This mill is why the buzzer grew SoundWave. Its lab proved that below ~60 Hz (Hz is vibrations per second) a square wave stops sounding like a tone and becomes a train of clicks to the ear. Deep bass therefore wants sine and triangle — and now the language has them.

How It's Built

The pure model is devils-dust-core.shoddy: the flock, the steering, the per-frame step, the knob table, and the meters. A wisp carries its trail and a finite loft — a buoyancy budget in frames — as data. While the loft lasts, the wisp rides the flock and the draft. When it runs out, the wisp drops out of the flock in the literal sense (separation, gravity, and flutter only) and rains down where it is. The census, order parameter, and the other meters are read-only folds — passes that boil a list down to a summary — over the same values the renderer draws.

The voice is devils-dust-sound.shoddy: a pure mapping half (drone pitch from drum speed, heat from the knobs, gains) tested headless — with no window and no sound device — and an effectful half that drives the buzzer. All of it is fire-and-forget: the calls return at once, because sound never blocks — it never makes the program wait.

The window half is devils-dust.shoddy: drawing, the console, and the event loop. A mouse click is classified once into a Hit value — hint, close box, tab, knob row, lever slot, stop bar, miss — and the handler is a flat Select Case over the result. Geometry lives in region predicates — yes/no words about where a point falls — that read as sentences. Every branch of the loop calls Loop directly: the self tail call (a word ending by calling itself) that the compiler turns into a loop.

Why More Than One File

The core never includes the scribbler or the buzzer. So all of it — steering, lifecycle, knob table, meters — runs headless under test.shoddy: seventy-three seeded checks with names like SPENT WOOL RAINS DOWN, ELEVEN IS ONE LOUDER THAN MAX, and THE CHURN IS A WHISPER. The sound file sits between: its mapping is pure and tested, and its buzzer calls are no-ops — calls that do nothing — when headless. The window file owns everything that needs a window. And sound-lab.shoddy is the spare room — the workbench where the drone's recipes were auditioned before any of them touched the mill.

The Machines It Uses

MachineWhy
seqIn the core: Filter and Fold are the flock — neighbours, steering sums, the census — and Taken caps each wisp's trail.
mathIn the core: Dist for neighbourhoods, Clamp/Lerp for rails and glides, Smoothstep shapes the devil's updraft, Sgn picks the outward wind, and the Rand* family spawns, flutters, and tumbles.
scribblerIn the shell: the window, the fixed-FPS tick queue, and the drawing words — trails via DrawLine, the devil via circles and rects, the console via FillRect and the 8×8 text.
buzzerIn the voice: held notes retuned every frame for the drone (NoteOn on a sounding channel is a pure pitch change), SoundWave for the sine/triangle bed, Play for the lever phrases.
keysIn the shell: ClassifyKey folds raw codes into the GameKey the keyboard dispatch matches on.
strIn the shell: PadLeft and ToFixed keep the knob readouts and footer gauges fixed-width, so the monospace columns never jitter.