Remixed Toung Twister

This interactive sketch loads a tongue-twister poem, breaks it into individual words, and shuffles them into a new random arrangement every time you click the canvas. The remixed words are displayed as readable lines of text, creating absurd and funny new versions of the original poem.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the background color — The RGB values in background() control the dark blue—try warmer colors like orange or purple to change the mood.
  2. Make the title pink — Changing the fill color before drawing the title text will make it display in a different color.
  3. Narrow the poem to half width — Constraining the text box width forces the poem to wrap into more lines, creating narrower columns.
  4. Make every 4 words break a line — Changing WORDS_PER_LINE creates shorter lines and a narrower poem layout.
  5. Make the poem text huge — Increasing textSize() makes the remixed poem more dramatic and easier to read from across a room.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates an interactive poetry remixer that takes the classic Betty Botter tongue-twister, breaks it into individual words, and rearranges them randomly on demand. Every click generates a fresh remix displayed on screen. It demonstrates several essential p5.js techniques: loading external text files with preload(), parsing text into words with string methods and splitTokens(), using arrays to store and shuffle data, and responding to user interaction with mousePressed().

The code is organized into four main pieces: preload() loads the poem file before anything else runs, setup() prepares the canvas and processes the poem into a word list, remixPoem() shuffles those words and formats them into readable lines, and draw() displays the result. By studying this sketch you'll learn how real creative coding projects load and manipulate text data, and how the noLoop() and redraw() pattern lets you update the canvas only when something meaningful changes rather than every frame.

⚙️ How It Works

  1. When the sketch loads, preload() runs first and fetches the poem.txt file from the server, storing each line in the poemLines array.
  2. setup() then creates a full-window canvas, processes the poem by joining all lines together and splitting them into individual words (stored in the words array), and calls remixPoem() to generate the first random arrangement.
  3. remixPoem() creates a shuffled copy of the words array using the shuffle() function, then loops through each shuffled word, concatenating them into a single string with spaces between words and newlines every 8 words to keep lines readable.
  4. draw() paints a dark background, draws a title explaining the interaction, and displays the current remixed poem using the text() function with a width constraint so it wraps properly.
  5. Whenever you click anywhere on the canvas, mousePressed() calls remixPoem() again, which shuffles the words in a new random order and calls redraw() to update the display—all other times, the canvas stays still because noLoop() prevents the draw loop from running continuously.

🎓 Concepts You'll Learn

File loading with preload()String manipulationArray shufflingText parsing with splitTokens()User interaction with mousePressed()noLoop() and redraw() patternResponsive canvas sizing

📝 Code Breakdown

preload()

preload() is a special p5.js function that runs once before setup(). Use it to load images, sounds, and text files—p5.js will wait for them to finish loading before moving on, preventing crashes from missing data.

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');
Fetches the poem.txt file from your project folder and stores each line as a separate element in the poemLines array. This happens BEFORE setup() runs, which guarantees the file is ready to use.

setup()

setup() runs once when the sketch starts. It's where you initialize your canvas, load data, set up initial values, and prepare everything before the main loop begins. The noLoop() call here is the key to this sketch's efficiency—we only redraw when the user clicks, not every frame.

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);
Creates a canvas that fills the entire browser window, using built-in p5 variables windowWidth and windowHeight for the dimensions.
textSize(24);
Sets the default font size to 24 pixels for all text drawn after this line.
textAlign(LEFT, TOP);
Aligns text so the top-left corner of text appears at the x,y coordinates you provide to text()—makes positioning predictable.
fill(255);
Sets the fill color to white (255, 255, 255 in RGB) for all shapes and text drawn after this.
noLoop();
Stops the draw() function from running continuously every frame—instead it only runs when you call redraw() manually, saving CPU power.
processPoemIntoWords();
Calls the helper function that breaks the loaded poem into individual words and stores them in the words array.
remixPoem();
Calls the function that shuffles the words and creates the first remixed poem, then redraws the canvas to display it.

processPoemIntoWords()

This function does the heavy lifting of text processing. It takes raw text (multiple lines with punctuation) and converts it into a clean array of individual words. Understanding splitTokens() and array methods like join(), filter(), and split() is essential for any text-based creative coding project.

🔬 This filter removes empty words. What if you changed word.length > 0 to word.length > 2? How many words would disappear and why?

  // Remove any empty strings that might have snuck in
  words = words.filter(word => word.length > 0);
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, " ,.

	'''");
  
  // Remove any empty strings that might have snuck in
  words = words.filter(word => word.length > 0);
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Join all lines const allText = poemLines.join(' ');

Combines all the separate lines from poemLines into one long string with spaces between them

calculation Split into words words = splitTokens(allText, " ,. '''");

Breaks the long string into individual words by treating spaces and punctuation as separators, discarding the separators themselves

for-loop Remove empty words words = words.filter(word => word.length > 0);

Scans through the words array and keeps only non-empty strings, removing any accidental blank entries

const allText = poemLines.join(' ');
Takes the poemLines array (where each line of the poem is a separate element) and joins them all into one big string, with a space between each line.
words = splitTokens(allText, " ,. '''");
The p5.js function splitTokens() breaks the big string apart at every space, comma, period, newline, tab, and special quote character, returning an array of individual words without those separators.
words = words.filter(word => word.length > 0);
Loops through the words array and keeps only entries that have at least one character, removing any accidental empty strings that might have snuck in during tokenization.

remixPoem()

This is the core creative function—it takes a shuffled word list and formats it into readable lines. The modulo operator (%) is the secret: it lets you perform an action at regular intervals (every Nth item). The phrase shuffle().slice() is a p5.js pattern that means 'randomize a copy, leaving the original untouched.'

🔬 This loop builds the poem string word by word. What if you changed the % operator or WORDS_PER_LINE value? For example, what if you changed % WORDS_PER_LINE to % 3 to break lines every 3 words instead of 8?

  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:

calculation Shuffle word array const shuffled = shuffle(words.slice());

Creates a randomized copy of the words array without changing the original, ready to be reassembled into a new poem

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

Loops through each shuffled word and concatenates it into the result string

conditional Format lines if ((i + 1) % WORDS_PER_LINE === 0) {

After every 8th word, adds a newline; otherwise adds a space—creating readable lines instead of one giant paragraph

const shuffled = shuffle(words.slice());
shuffle() is a p5.js function that randomizes the order of an array. The .slice() part makes a copy of the words array first, so the original stays unchanged and can be shuffled fresh next time.
let result = "";
Creates an empty string that will gradually build up the remixed poem as we loop through the shuffled words.
for (let i = 0; i < shuffled.length; i++) {
Loops through every word in the shuffled array, starting at index 0 and going until we reach the last word.
result += shuffled[i];
Appends the current word to the result string—the += operator means 'add to the end of'.
if ((i + 1) % WORDS_PER_LINE === 0) {
Checks if we've just added a word that's at a position divisible by WORDS_PER_LINE (every 8th word: 1-8, 9-16, etc.). The (i + 1) part adjusts for the fact that arrays start counting at 0.
result += "\n";
Adds a newline character to result, moving to the next line of text.
result += " ";
If we haven't hit a line break yet, adds a space after the word to separate it from the next one.
remixedPoem = result;
Stores the completed remixed poem string in the global variable remixedPoem, making it available for draw() to display.
redraw();
Calls draw() once to update the canvas with the new remixed poem—without this, nothing would appear until the next frame (but noLoop() prevents automatic frames).

draw()

draw() displays everything on screen. Because we called noLoop() in setup(), this function only runs when you call redraw() from remixPoem() or when the window resizes. The text() function's extra two parameters (width and height) make it constrain and wrap text within a box—essential for readable layouts.

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)

🔧 Subcomponents:

calculation Clear canvas background(20, 25, 35);

Paints the entire canvas a dark blue-gray, erasing everything from the previous frame

calculation Draw title text text("Random Tongue-Twister Remix", width/2, 30);

Displays the title centered at the top of the canvas

calculation Draw instructions text("Click anywhere for a new remix!", width/2, 55);

Shows instructions telling the user how to interact with the sketch

calculation Draw remixed poem text(remixedPoem, margin, 90, width - margin * 2, height - 120);

Displays the remixed poem in a constrained box with wrapping, using the global remixedPoem string

background(20, 25, 35);
Fills the entire canvas with a dark color (RGB: 20 red, 25 green, 35 blue), clearing whatever was drawn before.
const margin = 60;
Creates a local variable that defines the distance in pixels from the canvas edge to the text—keeps the poem from running all the way to the border.
fill(150, 200, 255);
Sets the text color to a light blue for the title and instructions.
textSize(18);
Sets the font size to 18 pixels for the title lines.
textAlign(CENTER);
Aligns the following text so its horizontal center falls at the x coordinate (width/2).
text("Random Tongue-Twister Remix", width/2, 30);
Draws the title text centered horizontally at the top of the canvas (x = width/2, y = 30).
text("Click anywhere for a new remix!", width/2, 55);
Draws the instruction text centered horizontally just below the title (x = width/2, y = 55).
fill(255);
Changes the fill color to white (255, 255, 255) for the poem text.
textSize(22);
Increases the font size to 22 pixels so the remixed poem is easier to read.
textAlign(LEFT, TOP);
Aligns text so the top-left corner is at the x,y coordinate—makes the poem layout predictable.
text(remixedPoem, margin, 90, width - margin * 2, height - 120);
Draws the remixed poem starting at coordinates (margin, 90) with a constrained width of (width - margin * 2) and height of (height - 120), causing text to wrap within the box.

mousePressed()

mousePressed() is a built-in p5.js event handler—p5.js automatically calls it whenever a mouse click happens. This is how you add interactivity to your sketches. Other common event handlers include mouseMoved(), keyPressed(), and windowResized().

// Click anywhere to generate a brand-new random tongue-twister remix
function mousePressed() {
  remixPoem();
}
Line-by-line explanation (2 lines)
function mousePressed() {
This special p5.js function automatically runs whenever the user clicks anywhere on the canvas or window.
remixPoem();
Calls the remixPoem function to shuffle the words and create a new remix, then redraw the canvas.

windowResized()

windowResized() is another built-in p5.js event handler that fires when the user resizes their browser window. Without this function, the canvas would stay at whatever size setup() created it, leading to ugly gaps or overflow. Always include it when you use createCanvas(windowWidth, windowHeight).

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  redraw(); // so the poem reflows to the new size
}
Line-by-line explanation (3 lines)
function windowResized() {
This special p5.js function automatically runs whenever the browser window is resized by the user.
resizeCanvas(windowWidth, windowHeight);
Resizes the p5 canvas to match the new window dimensions, preventing the canvas from becoming smaller or staying a fixed size.
redraw();
Triggers a call to draw() to repaint the canvas at the new size, causing the poem text to reflow and fit the new layout.

📦 Key Variables

poemLines array

Stores each line of the loaded poem as a separate array element. Loaded by preload() from poem.txt and used by processPoemIntoWords().

let poemLines;
words array

Stores all individual words from the poem after punctuation is stripped. Created by processPoemIntoWords() and shuffled by remixPoem().

let words = [];
remixedPoem string

Stores the current remixed poem as a single formatted string with newlines and spaces. Created by remixPoem() and displayed by draw().

let remixedPoem = "";
WORDS_PER_LINE number

A constant that controls how many words appear on each line of the remix before wrapping to the next line. Used in remixPoem() to format the output.

const WORDS_PER_LINE = 8;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG preload() and remixPoem()

If poem.txt is missing or fails to load, the sketch will crash silently with an empty words array, making remixes appear blank

💡 Add error handling: check if poemLines is null after preload, and provide a fallback poem. For example: if (!poemLines) { poemLines = ['The Betty Botter poem could not be loaded']; }

PERFORMANCE draw()

Text is rendered at full quality every frame even though noLoop() means it rarely changes, wasting GPU cycles on resize events

💡 This is actually efficient as-is because redraw() only calls draw() once. However, if you switch to loop(), consider using createGraphics() to cache the poem text as an image and redraw it only on remix.

STYLE remixPoem()

The string concatenation with += in a loop (concatenating 100+ times) is inefficient in JavaScript—best practice uses an array and join()

💡 Replace the loop with an array approach: for each word, add it to an array, then join with spaces and newlines at the end. This is much faster for large texts.

FEATURE remixPoem()

Every remix uses the same WORDS_PER_LINE value—no way for users to adjust line width without editing code

💡 Make WORDS_PER_LINE interactive: add global variables for user-controlled values and update draw() to display sliders or key controls. For example, pressing up/down arrows could change line length in real-time.

🔄 Code Flow

Code flow showing preload, setup, processpoemintwords, 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 --> background-clear[Clear Canvas] draw --> draw-title[Draw Title Text] draw --> draw-instructions[Draw Instructions] draw --> draw-poem[Draw Remixed Poem] draw-poem --> processpoemintwords[processpoemintwords] processpoemintwords --> join-lines[Join Lines] join-lines --> split-tokens[Split into Words] split-tokens --> filter-empty[Remove Empty Words] filter-empty --> shuffle-words[Shuffle Word Array] shuffle-words --> build-poem-loop[Build Remixed String] build-poem-loop --> add-newline-check[Format Lines] add-newline-check --> draw-poem click setup href "#fn-setup" click draw href "#fn-draw" click background-clear href "#sub-background-clear" click draw-title href "#sub-draw-title" click draw-instructions href "#sub-draw-instructions" click draw-poem href "#sub-draw-poem" click processpoemintwords href "#fn-processpoemintwords" click join-lines href "#sub-join-lines" click split-tokens href "#sub-split-tokens" click filter-empty href "#sub-filter-empty" click shuffle-words href "#sub-shuffle-words" click build-poem-loop href "#sub-build-poem-loop" click add-newline-check href "#sub-add-newline-check"

Preview

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