Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
We draw the track as a polyline (sampled points) and orient all glyphs by rotating to the tangent (rotate(p.ang)).
// Parametric curve + tangent
x = a * sin(t), y = (b/2) * sin(2t)
function infinityPoint(tau, a, b) {
const x = a * sin(tau);
const y = (b * 0.5) * sin(2 * tau);
const dx = a * cos(tau);
const dy = b * cos(2 * tau);
const ang = atan2(dy, dx); // tangent angle
return { x, y, ang };
}
Seasons = Turtle feeding cycles
Fin Strokes = Distance Measuring
Breath = Religious Timing/Calendar
Blink = Day/Night Cycle
// Parametric curve + tangent
x = a * sin(t), y = (b/2) * sin(2t)
function infinityPoint(tau, a, b) {
const x = a * sin(tau);
const y = (b * 0.5) * sin(2 * tau);
const dx = a * cos(tau);
const dy = b * cos(2 * tau);
const ang = atan2(dy, dx); // tangent angle
return { x, y, ang };
}
Track Color Season color slowly shifts via four stops using seasonalHue() and wrap-aware lerpHue()
const seasonHue = seasonalHue(seasonCyc); // amber → green → violet → cyan
stroke(seasonHue, …); drawInfinityPolyline(...);
Track Color: Season color slowly shifts via four stops using seasonalHue() and wrap-aware lerpHue()
const seasonHue = seasonalHue(seasonCyc); // amber → green → violet → cyan
stroke(seasonHue, …); drawInfinityPolyline(...);An image of MF DOOM's iconic mask is divided into a grid of tiles. When a button is pressed, random hidden tiles are revealed, and newly revealed tiles briefly glow in response to the sound.
revealed[r][c] = true;This project sits somewhere between an instrument, a game, and a piece of interactive visual art.
It asks a simple question:
What if making music wasn’t about building something new — but revealing something that already exists?
By tying sound, touch, and imagery together, the machine transforms rhythm into progress.
Sometimes, the beat isn’t the point. Sometimes, the beat is the key.
Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
At the heart of the system is a custom-built controller powered by an Arduino ESP32 Feather housed in a cardboard enclosure.
The interface includes:
.jpg)


On the hardware side, the ESP32 reads four buttons and a B10K slide potentiometer. Each loop, it sends their values as a single line of comma-separated data over USB serial.
#define B1 13
#define B2 12
#define B3 27
#define B4 33
#define POT_PIN 32
void loop() {
Serial.print(digitalRead(B1));
Serial.print(',');
Serial.print(digitalRead(B2));
Serial.print(',');
Serial.print(digitalRead(B3));
Serial.print(',');
Serial.print(digitalRead(B4));
Serial.print(',');
Serial.println(analogRead(POT_PIN));
}
In Processing, serial data is parsed and mapped to musical behavior. Each button triggers a drum sample, while the slider controls pitch by adjusting playback rate.
pitchRate = map(potRaw, 0, 4095, 0.5, 2.0);
pitchRate = constrain(pitchRate, 0.5, 2.0);
drums[i].rate(pitchRate);
drums[i].play();
An image of MF DOOM's iconic mask is divided into a grid of tiles. When a button is pressed, random hidden tiles are revealed, and newly revealed tiles briefly glow in response to the sound.
revealed[r][c] = true;On the hardware side, the ESP32 reads four buttons and a B10K slide potentiometer. Each loop, it sends their values as a single line of comma-separated data over USB serial.
#define B1 13
#define B2 12
#define B3 27
#define B4 33
#define POT_PIN 32
void loop() {
Serial.print(digitalRead(B1));
Serial.print(',');
Serial.print(digitalRead(B2));
Serial.print(',');
Serial.print(digitalRead(B3));
Serial.print(',');
Serial.print(digitalRead(B4));
Serial.print(',');
Serial.println(analogRead(POT_PIN));
}
This project sits somewhere between an instrument, a game, and a piece of interactive visual art.
It asks a simple question:
What if making music wasn’t about building something new — but revealing something that already exists?
By tying sound, touch, and imagery together, the machine transforms rhythm into progress.
Sometimes, the beat isn’t the point. Sometimes, the beat is the key.
Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseColForest – 2D fields for willow and aspen (0–1). Growth moves toward a cap reduced by barren; blocked under lodges; harvest() does a ring search for richest patch.DamSpans – spans are {row, cL, cR, integrity} across a gap bounded by land. Integrity decays with time and high flow; collapsed spans are removed.Colony, Beaver – array of agents; mortality filter; metrics; per-beaver FSM with vector steering.Lodge – simple sprite; count tied to populationWhy this order: environment first, then resources, then structures that shape flow, then agents reacting to the current state.
Creates subsystems and enforces the following update order (water → resources → structures → agents).
class World {
constructor(hud){
this.hud=hud;
this.hydro=new Hydro(hud);
this.forest=new Forest(hud,this.hydro);
this.dams=new DamSpans(hud,this.hydro);
this.colony=new Colony(hud,this.hydro,this.dams,this.forest);
this.lodges=[]; this.updateLodges(true);
}
update(){
this.hydro.update(); // rivers grow/branch/widen/dry
this.forest.update(this.lodges,this.hydro); // growth capped by barren
this.dams.update(this.hydro); // span decay vs flow
this.colony.update(this.dams,this.hydro); // FSM + mortality
if (this.colony.prunedThisTick){ this.updateLodges(); this.colony.prunedThisTick=false; }
this.forest.blockUnderLodges(this.lodges,this.hydro);
}
}The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Beavers “feel” water via noiseCol[c] (stress driver).
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Willows (for building dams) and Aspen (food for the beavers) grow on land toward a cap that shrinks with barren; growth is blocked under lodges and zeroed on water.The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Resulting effects: near water → fertile patches; dry right side → patchy, low-cap growth.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Dam Spans exist only across contiguous water segments bounded by land; integrity increases with work and decays with flow.
This prevents the beavers from spam building, and makes dams visually & mechanically legible.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Beavers are need-driven agents with vector steering.
Input from the right-side menu allows the user to shape conditions.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
G generate new river; SPACE pause.Hydro.target
This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.
Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseColForest – 2D fields for willow and aspen (0–1). Growth moves toward a cap reduced by barren; blocked under lodges; harvest() does a ring search for richest patch.DamSpans – spans are {row, cL, cR, integrity} across a gap bounded by land. Integrity decays with time and high flow; collapsed spans are removed.Colony, Beaver – array of agents; mortality filter; metrics; per-beaver FSM with vector steering.Lodge – simple sprite; count tied to populationWhy this order: environment first, then resources, then structures that shape flow, then agents reacting to the current state.
Creates subsystems and enforces the following update order (water → resources → structures → agents).
class World {
constructor(hud){
this.hud=hud;
this.hydro=new Hydro(hud);
this.forest=new Forest(hud,this.hydro);
this.dams=new DamSpans(hud,this.hydro);
this.colony=new Colony(hud,this.hydro,this.dams,this.forest);
this.lodges=[]; this.updateLodges(true);
}
update(){
this.hydro.update(); // rivers grow/branch/widen/dry
this.forest.update(this.lodges,this.hydro); // growth capped by barren
this.dams.update(this.hydro); // span decay vs flow
this.colony.update(this.dams,this.hydro); // FSM + mortality
if (this.colony.prunedThisTick){ this.updateLodges(); this.colony.prunedThisTick=false; }
this.forest.blockUnderLodges(this.lodges,this.hydro);
}
}The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Beavers “feel” water via noiseCol[c] (stress driver).
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Willows (for building dams) and Aspen (food for the beavers) grow on land toward a cap that shrinks with barren; growth is blocked under lodges and zeroed on water.The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Resulting effects: near water → fertile patches; dry right side → patchy, low-cap growth.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Dam Spans exist only across contiguous water segments bounded by land; integrity increases with work and decays with flow.
This prevents the beavers from spam building, and makes dams visually & mechanically legible.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Beavers are need-driven agents with vector steering.
Input from the right-side menu allows the user to shape conditions.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
G generate new river; SPACE pause.Hydro.target
This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.
Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseColForest – 2D fields for willow and aspen (0–1). Growth moves toward a cap reduced by barren; blocked under lodges; harvest() does a ring search for richest patch.DamSpans – spans are {row, cL, cR, integrity} across a gap bounded by land. Integrity decays with time and high flow; collapsed spans are removed.Colony, Beaver – array of agents; mortality filter; metrics; per-beaver FSM with vector steering.Lodge – simple sprite; count tied to populationWhy this order: environment first, then resources, then structures that shape flow, then agents reacting to the current state.
Creates subsystems and enforces the following update order (water → resources → structures → agents).
class World {
constructor(hud){
this.hud=hud;
this.hydro=new Hydro(hud);
this.forest=new Forest(hud,this.hydro);
this.dams=new DamSpans(hud,this.hydro);
this.colony=new Colony(hud,this.hydro,this.dams,this.forest);
this.lodges=[]; this.updateLodges(true);
}
update(){
this.hydro.update(); // rivers grow/branch/widen/dry
this.forest.update(this.lodges,this.hydro); // growth capped by barren
this.dams.update(this.hydro); // span decay vs flow
this.colony.update(this.dams,this.hydro); // FSM + mortality
if (this.colony.prunedThisTick){ this.updateLodges(); this.colony.prunedThisTick=false; }
this.forest.blockUnderLodges(this.lodges,this.hydro);
}
}The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Beavers “feel” water via noiseCol[c] (stress driver).
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Willows (for building dams) and Aspen (food for the beavers) grow on land toward a cap that shrinks with barren; growth is blocked under lodges and zeroed on water.The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Resulting effects: near water → fertile patches; dry right side → patchy, low-cap growth.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Dam Spans exist only across contiguous water segments bounded by land; integrity increases with work and decays with flow.
This prevents the beavers from spam building, and makes dams visually & mechanically legible.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Beavers are need-driven agents with vector steering.
Input from the right-side menu allows the user to shape conditions.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
G generate new river; SPACE pause.Hydro.target
This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.
Three what if scenarios for storytelling, and one mix-up story

Dec 21, 2024
Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseColForest – 2D fields for willow and aspen (0–1). Growth moves toward a cap reduced by barren; blocked under lodges; harvest() does a ring search for richest patch.DamSpans – spans are {row, cL, cR, integrity} across a gap bounded by land. Integrity decays with time and high flow; collapsed spans are removed.Colony, Beaver – array of agents; mortality filter; metrics; per-beaver FSM with vector steering.Lodge – simple sprite; count tied to populationWhy this order: environment first, then resources, then structures that shape flow, then agents reacting to the current state.
Creates subsystems and enforces the following update order (water → resources → structures → agents).
class World {
constructor(hud){
this.hud=hud;
this.hydro=new Hydro(hud);
this.forest=new Forest(hud,this.hydro);
this.dams=new DamSpans(hud,this.hydro);
this.colony=new Colony(hud,this.hydro,this.dams,this.forest);
this.lodges=[]; this.updateLodges(true);
}
update(){
this.hydro.update(); // rivers grow/branch/widen/dry
this.forest.update(this.lodges,this.hydro); // growth capped by barren
this.dams.update(this.hydro); // span decay vs flow
this.colony.update(this.dams,this.hydro); // FSM + mortality
if (this.colony.prunedThisTick){ this.updateLodges(); this.colony.prunedThisTick=false; }
this.forest.blockUnderLodges(this.lodges,this.hydro);
}
}The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Beavers “feel” water via noiseCol[c] (stress driver).
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Willows (for building dams) and Aspen (food for the beavers) grow on land toward a cap that shrinks with barren; growth is blocked under lodges and zeroed on water.The simulation starts with building a target river tree (trunk + noisy branches), reveals it over time (slider), then keep it alive with estuaries, widening, and right-side desiccation.
Resulting effects: near water → fertile patches; dry right side → patchy, low-cap growth.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Dam Spans exist only across contiguous water segments bounded by land; integrity increases with work and decays with flow.
This prevents the beavers from spam building, and makes dams visually & mechanically legible.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
Beavers are need-driven agents with vector steering.
Input from the right-side menu allows the user to shape conditions.
regen(){
this.water=grid(false); this.target=grid(false); this.barren=grid(0); this.growth=0;
const trunk = walkBranch(2, rows*0.5, cols*0.85, 0, true); paintPath(trunk,5);
for (let i=0;i<10;i++){ const pC=rand(6,cols*0.7), pR=clamp(noise(i*.3)*rows,2,rows-3);
paintPath(walkBranch(pC,pR,rand(18,48), coin()?-1:+1,false),3);
}
}
update(){
// reveal target → water
this.growth=min(1,(this.growth||0)+(0.002+hud.get("riverSpeed")*0.02));
for (let c=0;c<floor(cols*this.growth);c++) for (let r=0;r<rows;r++) if (target[c][r]) water[c][r]=true;
// desiccate far right → barren land
for (let c=floor(cols*.75); c<cols; c++) if (random()<0.0008)
for (let r=0;r<rows;r++) if (water[c][r]){ water[c][r]=target[c][r]=false; barren[c][r]=min(1,barren[c][r]+.4); }
// estuaries & widening (if undammed & old)
if (++branchTimer>240){ branchTimer=0; /* spawn small branch from wet pivot; paintPath(...,2) */ }
// …age water; leak to neighbors when age>180 && !damSpans.hasDamNear(c,r,4)
recalcColumns(); // sets velCol[], noiseCol[] from wet fraction per column
}
G generate new river; SPACE pause.Hydro.target
This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.