Shawns_Remix

This sketch loads the Betty Botter tongue-twister from a text file, breaks it into individual words, and shuffles them into a fresh random arrangement every time you click. The result is a playful word-remix poem that reflows to fit the browser window.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change words per line — Adjusting WORDS_PER_LINE reshapes the poem into shorter, choppier lines or longer, flowing ones.
  2. Recolor the poem — Swapping the fill color before drawing the poem body gives it a completely different visual tone.
  3. Make text bigger for punchier lines — Increasing textSize() makes the poem feel bolder and forces more line wrapping within the same box.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch turns the classic Betty Botter tongue-twister into an endless generator of nonsense poetry. It loads text from a file, splits it into individual words using splitTokens(), shuffles those words with p5's shuffle() function, and lays the result out as wrapped text on the canvas. Every click produces a brand new random arrangement, making it a fun demonstration of how simple array manipulation can create generative text art.

The code is organized around a load-once, remix-on-demand pattern: preload() fetches the poem file, setup() processes it into a word list and draws the first remix, and mousePressed() triggers new remixes without re-loading anything. Because the sketch uses noLoop() and redraw() instead of a continuous draw loop, it's also a great example of how to make an efficient, event-driven p5.js sketch rather than one that redraws 60 times a second unnecessarily.

⚙️ How It Works

  1. When the page loads, preload() reads poem.txt into an array of lines before anything else runs, guaranteeing the text is ready in time
  2. setup() creates a full-window canvas, sets text styles, calls noLoop() to stop automatic redrawing, then processes the poem into a flat array of words and generates the first random remix
  3. processPoemIntoWords() joins all the lines into one string and uses splitTokens() to break it apart at spaces, punctuation, and quote marks, leaving a clean array of individual words
  4. remixPoem() shuffles a copy of the word array, then loops through it building a text string that inserts a line break every 8 words instead of a space
  5. Because the sketch calls noLoop(), draw() only runs once automatically and then again only when redraw() is explicitly called
  6. Clicking anywhere triggers mousePressed(), which calls remixPoem() again to shuffle the words differently and redraw the new text; resizing the window similarly reflows the same words to the new canvas size

🎓 Concepts You'll Learn

Loading external text files (preload/loadStrings)String tokenizing (splitTokens)Array shufflingEvent-driven redraw with noLoop()Text wrapping and alignmentResponsive canvas with windowResized()

📝 Code Breakdown

preload()

preload() is a special p5.js function that runs before setup(). It's the standard place to load images, fonts, JSON, or text files so your sketch never tries to use data that hasn't arrived yet.

function preload() {
  // Load the poem as an array of lines
  // https://p5js.org/reference/#/p5/loadStrings
  poemLines = loadStrings('poem.txt');
}
Line-by-line explanation (1 lines)
poemLines = loadStrings('poem.txt');
p5.js automatically waits for this file to finish loading before running setup(), so poemLines is guaranteed to be filled with an array of text lines by the time it's needed.

setup()

setup() runs once when the sketch starts, making it the perfect place to configure the canvas, set defaults, and prepare any data your sketch needs before the first draw.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textSize(24);
  textAlign(LEFT, TOP);
  fill(255);
  noLoop(); // we'll redraw only when we generate a new remix

  processPoemIntoWords();
  remixPoem(); // generate the first random poem
}
Line-by-line explanation (7 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
textSize(24);
Sets a default text size (later overridden inside draw()).
textAlign(LEFT, TOP);
Sets text to be drawn starting from its left edge and top, which matters for positioning the poem block.
fill(255);
Sets the default drawing/text color to white.
noLoop();
Stops p5.js from calling draw() automatically 60 times per second - draw() will only run when redraw() is explicitly called, saving CPU since the poem doesn't need constant animation.
processPoemIntoWords();
Calls the helper function that turns the loaded poem text into an array of individual words.
remixPoem();
Generates and displays the very first random shuffle of the poem as soon as the sketch starts.

processPoemIntoWords()

This function shows a common text-processing pattern: join lines into one string, then use splitTokens() (p5's version of split with multiple delimiters) to tokenize it into words for further manipulation.

🔬 This line decides what counts as a 'separator' between words. What happens if you remove the comma from the separator string, so commas stay attached to words like 'Botter,'?

  words = splitTokens(allText, " ,.\n\r\t\f'''");
function processPoemIntoWords() {
  // Join all lines into one big string
  const allText = poemLines.join(' ');

  // Split into individual words, removing punctuation chars as separators
  // https://p5js.org/reference/#/p5/splitTokens
  // We treat spaces, punctuation, and various quotes as token separators.
  words = splitTokens(allText, " ,.\n\r\t\f'''");
  
  // Remove any empty strings that might have snuck in
  words = words.filter(word => word.length > 0);
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Remove Empty Strings words = words.filter(word => word.length > 0);

Cleans up the word array by discarding any zero-length strings that splitTokens might produce from consecutive separators

const allText = poemLines.join(' ');
Combines the array of lines (loaded by loadStrings) into a single long string, with a space between each line.
words = splitTokens(allText, " ,.\n\r\t\f'''");
Breaks the big string apart wherever it finds any of the listed characters (spaces, commas, periods, newlines, tabs, quotes), leaving just an array of clean words.
words = words.filter(word => word.length > 0);
Uses JavaScript's array filter() to throw away any empty strings that can appear when two separator characters sit next to each other.

remixPoem()

This function demonstrates the classic 'shuffle an array, then rebuild a string' pattern used in generative text art, and shows how the modulo operator (%) is used to trigger periodic actions inside a loop.

🔬 This loop decides spacing and line breaks. What happens if you insert a random chance of adding punctuation, like a comma, after some words instead of always a space?

  for (let i = 0; i < shuffled.length; i++) {
    result += shuffled[i];

    // Add a newline every WORDS_PER_LINE words, otherwise a space
    if ((i + 1) % WORDS_PER_LINE === 0) {
      result += "\n";
    } else {
      result += " ";
    }
  }
function remixPoem() {
  // Create a shuffled copy of the words array
  // https://p5js.org/reference/#/p5/shuffle
  const shuffled = shuffle(words.slice()); // slice() to avoid mutating original

  let result = "";
  for (let i = 0; i < shuffled.length; i++) {
    result += shuffled[i];

    // Add a newline every WORDS_PER_LINE words, otherwise a space
    if ((i + 1) % WORDS_PER_LINE === 0) {
      result += "\n";
    } else {
      result += " ";
    }
  }

  remixedPoem = result;
  redraw(); // trigger a single call to draw()
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

for-loop Build Poem String for (let i = 0; i < shuffled.length; i++) {

Walks through every shuffled word and appends it to the result string, inserting line breaks at regular intervals

conditional Line Break Check if ((i + 1) % WORDS_PER_LINE === 0) {

Uses the modulo operator to decide when enough words have been added to start a new line

const shuffled = shuffle(words.slice());
words.slice() makes a copy of the original array so shuffling doesn't scramble the master word list; shuffle() then randomly reorders that copy.
let result = "";
Starts an empty string that will be built up word by word.
for (let i = 0; i < shuffled.length; i++) {
Loops through every word in the shuffled array, one at a time, tracking position with i.
result += shuffled[i];
Appends the current word onto the growing result string.
if ((i + 1) % WORDS_PER_LINE === 0) {
The modulo operator % checks whether the word count so far is an exact multiple of WORDS_PER_LINE (8) - if so it's time for a line break.
result += "\n";
Adds a newline character to start a fresh line in the text block.
result += " ";
Otherwise just adds a space so the next word doesn't run into this one.
remixedPoem = result;
Saves the finished string into the global variable that draw() displays.
redraw();
Since the sketch uses noLoop(), this manually triggers one call to draw() so the new poem appears immediately.

draw()

draw() normally loops continuously, but here it's only called once at startup and then again whenever redraw() is triggered, which is an efficient pattern for sketches that don't need constant animation.

function draw() {
  background(20, 25, 35);
  const margin = 60;

  // Title
  fill(150, 200, 255);
  textSize(18);
  textAlign(CENTER);
  text("Random Tongue-Twister Remix", width/2, 30);
  text("Click anywhere for a new remix!", width/2, 55);

  // Draw the remixed poem
  fill(255);
  textSize(22);
  textAlign(LEFT, TOP);
  text(remixedPoem, margin, 90, width - margin * 2, height - 120);
}
Line-by-line explanation (11 lines)
background(20, 25, 35);
Fills the whole canvas with a dark navy color to clear the previous frame before redrawing.
const margin = 60;
Defines how much empty space to leave around the poem text block, in pixels.
fill(150, 200, 255);
Sets the color for the title text to a light blue.
textSize(18);
Sets a smaller text size just for the title lines.
textAlign(CENTER);
Centers the following text horizontally around the given x coordinate.
text("Random Tongue-Twister Remix", width/2, 30);
Draws the title centered horizontally near the top of the canvas.
text("Click anywhere for a new remix!", width/2, 55);
Draws a subtitle instructing the user how to interact with the sketch.
fill(255);
Switches the fill color to white for the main poem text.
textSize(22);
Sets a larger text size for the poem body.
textAlign(LEFT, TOP);
Resets alignment so the poem text box positions from its top-left corner.
text(remixedPoem, margin, 90, width - margin * 2, height - 120);
Draws the whole remixed poem string inside an invisible box - the extra width/height arguments make p5.js automatically wrap long text within that box.

mousePressed()

mousePressed() is a p5.js event function that automatically runs whenever the user clicks. It's the standard way to add simple click-based interaction without writing your own event listeners.

// Click anywhere to generate a brand-new random tongue-twister remix
function mousePressed() {
  remixPoem();
}
Line-by-line explanation (1 lines)
remixPoem();
Every time the mouse is clicked anywhere on the canvas, this calls remixPoem() again to shuffle the words differently and redraw the new arrangement.

windowResized()

windowResized() is a p5.js event function that fires automatically when the browser window changes size, letting you keep responsive sketches that adapt to any screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  redraw(); // so the poem reflows to the new size
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window's new dimensions whenever it changes.
redraw();
Since noLoop() stopped automatic redrawing, this manually forces one redraw so the poem text reflows correctly to fit the new canvas size.

📦 Key Variables

poemLines array

Stores the raw lines of text loaded from poem.txt before they're joined and tokenized into words

let poemLines;
words array

Holds every individual word from the poem after punctuation and whitespace have been stripped out

let words = [];
remixedPoem string

Stores the current shuffled poem as one string with line breaks, ready to be displayed by draw()

let remixedPoem = "";
WORDS_PER_LINE number

A constant controlling how many words appear on each line before a line break is inserted

const WORDS_PER_LINE = 8;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG processPoemIntoWords()

The splitTokens separator string " ,.\n\r\t\f'''" contains three identical apostrophe characters in a row instead of distinct curly-quote characters, so it likely doesn't strip smart quotes (' ' " ") that might appear in poem.txt, leaving stray quote marks attached to words.

💡 Explicitly include the actual Unicode curly quote characters (e.g. \u2018\u2019\u201C\u201D) in the separator string, or strip punctuation with a regex replace before tokenizing.

FEATURE remixPoem()

Every remix uses a fixed WORDS_PER_LINE, so line length never varies and the visual rhythm is always the same.

💡 Randomize the words-per-line count within a range (e.g. 5-10) each time remixPoem() runs for more visually varied poem shapes.

STYLE draw()

Layout numbers like margin, 90, 30, 55, and 120 are hardcoded magic numbers scattered through the function, making it harder to adjust the layout consistently.

💡 Pull these into named constants (e.g. TITLE_Y, POEM_TOP, BOTTOM_PADDING) declared near the top of the file so layout tweaks only need to happen in one place.

FEATURE mousePressed()

There's no visual feedback or transition between remixes - the text just instantly swaps, which can feel abrupt.

💡 Add a simple fade or highlight animation when new words appear, or briefly flash the background color on click to signal the action registered.

🔄 Code Flow

Code flow showing preload, setup, processpoemintowords, remixpoem, draw, mousepressed, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> processpoemintowords[processpoemintowords] processpoemintowords --> filter-empty-words[filter-empty-words] filter-empty-words --> remixpoem[remixpoem] remixpoem --> build-result-loop[build-result-loop] build-result-loop --> linebreak-check[linebreak-check] linebreak-check --> build-result-loop build-result-loop --> draw click setup href "#fn-setup" click draw href "#fn-draw" click processpoemintowords href "#fn-processpoemintowords" click filter-empty-words href "#sub-filter-empty-words" click remixpoem href "#fn-remixpoem" click build-result-loop href "#sub-build-result-loop" click linebreak-check href "#sub-linebreak-check"

Preview

Shawns_Remix - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Shawns_Remix - Code flow showing preload, setup, processpoemintowords, remixpoem, draw, mousepressed, windowresized
Code Flow Diagram