Ingredients of a Story

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Code Breakdown

Figure 8 Track

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 };
}

4-Behaviors

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 };
}

Visual Language

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;

Reflection

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.

Ingredients of a Story

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Ingredients of a Story

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Process

Physical Interface

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:

  • Four physical buttons
    • Each button triggers a different sound sample and reveals tiles from a specific region of the image.
  • One slide potentiometer
    • The slider controls pitch, shifting the musical mood from low and heavy to sharp and elevated. It also acts as a modifier, changing how the sounds feel without changing how they’re played.

Breadboard with inputs

Full view + Slide Potentiometer

DOOMBOX Exterior Housing

Arduino IDE: Reading the Interface

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));
}

Processing: Sound Playback and Pitch Control

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();

Processing: Image Reveal

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;

Processing: Image Reveal

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));
}

Reflection

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.

Logic be Dammed:
Representing Forces and Nature in Code

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Code Breakdown

Classes

  • Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseCol
    • Builds trunk + branches with Perlin noise and stochastic jitter.
    • Spawns new estuaries on a timer; widens old undammed channels; applies right-side desiccation and water-adjacent recovery.
  • Forest – 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 population

Making the world

Why 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);
  }
}

Rivers

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
}

Forests & Barren Tiles

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
}

Dams & Bank-to-Bank Spans

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
}

Agents & Interaction: Beavers, Vectors, HUD

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
}

Input & HUD

  • Keys: G generate new river; SPACE pause.
  • Mouse: Left = plant Willow, Shift+Left = plant Aspen; toggle Spawn Mode then Right-click to add a random beaver.
  • Sliders (stacked under descriptions; buttons below sliders—no overlap):
    1. River Creation Speed → reveal rate of Hydro.target
    2. Food Spawn Rate (Aspen) → forest growth term
    3. Building Material Spawn Rate (Willow) → forest growth term
    4. Dam Building Speed → per-action span integrity

This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.

Logic be Dammed:
Representing Forces and Nature in Code

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Code Breakdown

Classes

  • Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseCol
    • Builds trunk + branches with Perlin noise and stochastic jitter.
    • Spawns new estuaries on a timer; widens old undammed channels; applies right-side desiccation and water-adjacent recovery.
  • Forest – 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 population

Making the world

Why 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);
  }
}

Rivers

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
}

Forests & Barren Tiles

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
}

Dams & Bank-to-Bank Spans

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
}

Agents & Interaction: Beavers, Vectors, HUD

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
}

Input & HUD

  • Keys: G generate new river; SPACE pause.
  • Mouse: Left = plant Willow, Shift+Left = plant Aspen; toggle Spawn Mode then Right-click to add a random beaver.
  • Sliders (stacked under descriptions; buttons below sliders—no overlap):
    1. River Creation Speed → reveal rate of Hydro.target
    2. Food Spawn Rate (Aspen) → forest growth term
    3. Building Material Spawn Rate (Willow) → forest growth term
    4. Dam Building Speed → per-action span integrity

This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.

Logic be Dammed:
Representing Forces and Nature in Code

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Code Breakdown

Classes

  • Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseCol
    • Builds trunk + branches with Perlin noise and stochastic jitter.
    • Spawns new estuaries on a timer; widens old undammed channels; applies right-side desiccation and water-adjacent recovery.
  • Forest – 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 population

Making the world

Why 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);
  }
}

Rivers

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
}

Forests & Barren Tiles

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
}

Dams & Bank-to-Bank Spans

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
}

Agents & Interaction: Beavers, Vectors, HUD

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
}

Input & HUD

  • Keys: G generate new river; SPACE pause.
  • Mouse: Left = plant Willow, Shift+Left = plant Aspen; toggle Spawn Mode then Right-click to add a random beaver.
  • Sliders (stacked under descriptions; buttons below sliders—no overlap):
    1. River Creation Speed → reveal rate of Hydro.target
    2. Food Spawn Rate (Aspen) → forest growth term
    3. Building Material Spawn Rate (Willow) → forest growth term
    4. Dam Building Speed → per-action span integrity

This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.

Logic be Dammed:
Representing Forces and Nature in Code

Notable quotes and thoughts on Combining the Ingredients of a Story from the book Storytelling for User Experience.

Marcus Thomas

Dec 21, 2024

Intro:

Storytelling for User Experience is a book by Whitney Quesenbery & Kevin Brooks, as well as the main text for my Interactive Storytelling class. This weeks chapter focused on the ingredients of a story: perspective, characters, context, imagery, language of the story. As I read through this, I pulled some valuable insights that were both refreshing and gave me a lot to chew on; these are my thoughts on what resonated with me most.

Quote 1:

...when you are crafting a story, you start with the audience. Their relationship to the story, and your purpose in telling them the story, are an important part of your perspective.

Knowing one's audience is probably the most important part of storytelling, in my opinion. This quote is a concise insight into understanding that point, and I find it to be a true north for applying this chapter's principles. By nature, I think that we tend to start with our own why for telling our stories, but as UX designers, we are inherently creating products that come from our user's stories, and so it is even more critical that we start from looking outside ourselves and work back inwardly. The users perspectives build the frame for us, and so when we are painting a picture juxtaposing problem and solution, we cannot force our own perspective into the forefront, or else we run the risk of losing our audience before even hooking them in. By meditating on this quote, I realized, that by getting our audience to see themselves in the story we're telling, the solution(s) we present can give them a sense of catharsis as there is a less steep onramp to getting them onboard by the end of it.

Quote 2:

These should not be whimsical decisions, but careful choices to help craft the story.

In my first design job, I worked as a junior marketer for a comedy club. One of my tasks was video editing, where I would post great jokes on social media, as well edit down comedians sets so that they can put them into their reels and study what did or didn't work for different jokes. As such, I watched hundreds if not thousands of hours worth of material, and something that I became really adept at recognizing (particularly for posting a joke on social media) was not just the punchline, but the 'payoff' of when a comedian would tell a story. The difference being, having a great payoff at the end came from the comedians setup to the punchline(s) being a sequence of events that felt plausible, no matter the absurdity of the scenario described, and result in a feeling of there being a resolution, versus a punchline acting as punctuation for that resolution. For great storytellers, in the setup, the characters involved wouldn't do outlandish things just for the sake of something being 'a funny thing to do', but there was a logic behind their actions and reactions within context which would coincidentally be hilarious enough to adapt into a story.

This quote gave me an instant flashback to that time, and I think that translating that technique into use for UX is just as relevant for this profession. I agree with the notion that throwing in whimsical characters or anecdotes for the hell of it doesn't 'punch up' (as comedians say) a story, but rather distracts and can lead to losing your audience. The emotional payoff comes from the intentionality of descriptions and introduction of characters/scenarios/behaviors etc., and not from verbalized intrusive thoughts. In short, we as designers need to be hyper-aware that we're taking our audience on a journey, and not an odyssey.

Quote 3:

Imagery works with all the other ingredients—perspective, context, and character. It makes them memorable by adding word pictures that describe specific details.

To continue with my relation between this chapter and comedy, imagery is truly your bread and butter for 'punching up' your story telling. Finding a balance with adding imagery though is something that is in itself an art. If you add too much, you might just end up with something more akin to creative fiction instead of something grounded in reality, but given some flavor to make the story more easy to chew on and digest. On the opposite end, by adding too little, you end up with a professional briefing. Factual, cold, and disengaging. Having interesting imagery is critical to not fall into recounting events, and develop your story into something that moves people and causes your audience to take with them long after you've told it.

Final Thoughts:

None of these are extra—they are what makes something a story and not just a chronological report

As a final note, this quote gets to the heart of what these ingredients do. They're parts of a whole—inseparable and interweaved with each other—and are staples for serving up stories that touch, move, and inspire.

Code Breakdown

Classes

  • Hydro – tile grids for water, target, age, barren; column arrays for velCol, noiseCol
    • Builds trunk + branches with Perlin noise and stochastic jitter.
    • Spawns new estuaries on a timer; widens old undammed channels; applies right-side desiccation and water-adjacent recovery.
  • Forest – 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 population

Making the world

Why 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);
  }
}

Rivers

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
}

Forests & Barren Tiles

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
}

Dams & Bank-to-Bank Spans

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
}

Agents & Interaction: Beavers, Vectors, HUD

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
}

Input & HUD

  • Keys: G generate new river; SPACE pause.
  • Mouse: Left = plant Willow, Shift+Left = plant Aspen; toggle Spawn Mode then Right-click to add a random beaver.
  • Sliders (stacked under descriptions; buttons below sliders—no overlap):
    1. River Creation Speed → reveal rate of Hydro.target
    2. Food Spawn Rate (Aspen) → forest growth term
    3. Building Material Spawn Rate (Willow) → forest growth term
    4. Dam Building Speed → per-action span integrity

This closes the system loop: environment drives needs; agents act; structures reshape the environment; HUD lets you adjust parameters and watch new equilibria emerge.