Remixed Toung Twister

This interactive sketch loads a tongue-twister poem, breaks it into individual words, and randomly shuffles them to create hilarious remixed versions. Click anywhere on the canvas to generate a new random remix, with words automatically arranged into lines of 8 words each.

🎓 Concepts You'll Learn

File loading (preload)String manipulationArray shufflingText renderingEvent handlingResponsive canvasnoLoop() for performance

🔄 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 --> noloop-setup[noloop-setup] setup --> canvas-setup[canvas-setup] setup --> initialization-calls[initialization-calls] setup --> draw[draw loop] draw --> background-clear[background-clear] draw --> title-section[title-section] draw --> poem-section[poem-section] draw --> mousepressed[mousepressed] draw --> windowresized[windowresized] mousepressed --> remix-call[remix-call] remix-call --> remixpoem[remixpoem] remixpoem --> join-lines[join-lines] join-lines --> split-tokens[split-tokens] split-tokens --> filter-empty[filter-empty] filter-empty --> shuffle-words[shuffle-words] shuffle-words --> build-string-loop[build-string-loop] build-string-loop --> newline-check[newline-check] newline-check --> trigger-redraw[trigger-redraw] trigger-redraw --> draw windowresized --> canvas-resize[canvas-resize] canvas-resize --> reflow-text[reflow-text] reflow-text --> draw click setup href "#fn-setup" click draw href "#fn-draw" click mousepressed href "#fn-mousepressed" click windowresized href "#fn-windowresized" click noloop-setup href "#sub-noloop-setup" click canvas-setup href "#sub-canvas-setup" click initialization-calls href "#sub-initialization-calls" click background-clear href "#sub-background-clear" click title-section href "#sub-title-section" click poem-section href "#sub-poem-section" click remix-call href "#sub-remix-call" click remixpoem href "#fn-remixpoem" 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-string-loop href "#sub-build-string-loop" click newline-check href "#sub-newline-check" click trigger-redraw href "#sub-trigger-redraw" click canvas-resize href "#sub-canvas-resize" click reflow-text href "#sub-reflow-text"

📝 Code Breakdown

preload()

preload() is a special p5.js function that runs before setup(). Use it to load files, images, or sounds that your sketch needs. Without preload(), the sketch might try to use data before it's finished loading.

function preload() {
  // Load the poem as an array of lines
  // https://p5js.org/reference/#/p5/loadStrings
  poemLines = loadStrings('poem.txt');
}

đź”§ Subcomponents:

file-loading Load Poem File poemLines = loadStrings('poem.txt');

Reads the poem.txt file and stores each line as an element in the poemLines array

Line by Line:

poemLines = loadStrings('poem.txt');
loadStrings() is a p5.js function that reads a text file and returns an array where each element is one line from the file. This must happen in preload() so the file is loaded before setup() runs.

setup()

setup() runs once when the sketch starts. It's where you initialize your canvas, load data, and set up starting values. Using noLoop() here is a smart optimization because this sketch only needs to 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
}

đź”§ Subcomponents:

initialization Canvas and Text Configuration createCanvas(windowWidth, windowHeight); textSize(24); textAlign(LEFT, TOP); fill(255);

Sets up the canvas to fill the window, configures text properties (size, alignment, color)

performance-optimization Disable Continuous Drawing noLoop();

Stops draw() from running every frame; it will only run when redraw() is called, saving CPU

function-calls Initialize Poem Data and Generate First Remix processPoemIntoWords(); remixPoem();

Converts the loaded poem into a word array, then generates and displays the first random remix

Line by Line:

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window. windowWidth and windowHeight are p5.js variables that automatically update if the window is resized.
textSize(24);
Sets the default font size to 24 pixels for all text drawn after this line.
textAlign(LEFT, TOP);
Aligns text to the left horizontally and top vertically. This means when you draw text at position (x, y), the top-left corner of the text starts at that position.
fill(255);
Sets the fill color to white (255 in grayscale). This affects all text and shapes drawn afterward.
noLoop();
Stops the draw() function from running continuously. Instead, draw() only runs when redraw() is explicitly called, which is more efficient for this sketch.
processPoemIntoWords();
Calls the function that converts the loaded poem into an array of individual words.
remixPoem();
Calls the function that shuffles the words and creates the first remixed poem to display.

processPoemIntoWords()

This function transforms the raw poem text into a clean array of words. The key insight is that splitTokens() treats multiple separators as word boundaries, so punctuation attached to words gets removed automatically. The filter at the end is defensive programming—it catches edge cases where empty strings might slip through.

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

đź”§ Subcomponents:

string-operation Combine All Lines const allText = poemLines.join(' ');

Joins all lines of the poem into one long string, with spaces between lines

string-parsing Split into Words words = splitTokens(allText, " ,. '''");

Breaks the text into individual words, treating spaces and punctuation as separators

array-filtering Remove Empty Strings words = words.filter(word => word.length > 0);

Removes any empty strings that might have been created during splitting

Line by Line:

const allText = poemLines.join(' ');
join() combines all elements of the poemLines array into a single string, with a space between each line. For example, ['line1', 'line2'] becomes 'line1 line2'.
words = splitTokens(allText, " ,. '''");
splitTokens() breaks the text into words using the characters in the second parameter as separators. It treats spaces, commas, periods, newlines, tabs, and quote marks as word boundaries, so 'Betty,botter' becomes ['Betty', 'botter'].
words = words.filter(word => word.length > 0);
filter() keeps only words that have length > 0, removing any empty strings. This prevents blank words from being included in the remix.

remixPoem()

This is the core function that creates the remix. The shuffle() function is key—it randomly reorders the words each time. The loop that follows builds a formatted string with proper spacing and line breaks. Using redraw() instead of letting draw() run continuously saves CPU because we only need to update the display when the user clicks.

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

đź”§ Subcomponents:

array-operation Shuffle Word Array const shuffled = shuffle(words.slice());

Creates a randomized copy of the words array without changing the original

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

Loops through each shuffled word and builds the final poem string with proper spacing and line breaks

conditional Line Break Logic if ((i + 1) % WORDS_PER_LINE === 0) { result += "\n"; } else { result += " "; }

Adds a newline after every 8 words, otherwise adds a space between words

function-call Trigger Display Update remixedPoem = result; redraw();

Stores the new poem and tells p5.js to call draw() once to display it

Line by Line:

const shuffled = shuffle(words.slice());
shuffle() randomly rearranges an array. words.slice() creates a copy of the words array, so shuffle() randomizes the copy without changing the original words array. This is important because we want to shuffle differently each time remixPoem() is called.
let result = "";
Initializes an empty string that will hold the final remixed poem.
for (let i = 0; i < shuffled.length; i++) {
Loops through each word in the shuffled array, from index 0 to the end.
result += shuffled[i];
Adds the current word to the result string.
if ((i + 1) % WORDS_PER_LINE === 0) {
Checks if we've just added the 8th, 16th, 24th word, etc. (i+1 because i starts at 0). The % operator gives the remainder after division, so (i+1) % 8 === 0 is true every 8 words.
result += "\n";
If we've just completed a line of 8 words, add a newline character to move to the next line.
} else { result += " "; }
Otherwise, add a space between words on the same line.
remixedPoem = result;
Stores the newly built poem string in the global remixedPoem variable so draw() can display it.
redraw();
Since we used noLoop() in setup(), draw() doesn't run automatically. redraw() tells p5.js to call draw() once to display the new poem.

draw()

draw() is called whenever redraw() is invoked (in this sketch, only when the user clicks or the window resizes). It clears the canvas, draws the title and instructions, and displays the current remixed poem. The text() function with width and height parameters automatically wraps text to fit within the specified box.

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

đź”§ Subcomponents:

drawing-operation Clear Canvas background(20, 25, 35);

Fills the entire canvas with a dark blue-gray color, erasing previous content

text-rendering Draw Title and Instructions fill(150, 200, 255); textSize(18); textAlign(CENTER); text(...)

Displays the title and instructions in light blue at the top center of the canvas

text-rendering Draw Remixed Poem fill(255); textSize(22); textAlign(LEFT, TOP); text(remixedPoem, margin, 90, width - margin * 2, height - 120);

Displays the remixed poem in white text with margins and a constrained width for text wrapping

Line by Line:

background(20, 25, 35);
Fills the canvas with a dark blue-gray color (RGB: 20, 25, 35). This happens every frame draw() is called, clearing any previous drawings.
const margin = 60;
Creates a local constant for the margin size (60 pixels) used to position text away from the canvas edges.
fill(150, 200, 255);
Sets the text color to light blue (RGB: 150, 200, 255) for the title.
textSize(18);
Sets the font size to 18 pixels for the title text.
textAlign(CENTER);
Aligns the title text to the center horizontally. The vertical alignment stays as TOP from setup().
text("Random Tongue-Twister Remix", width/2, 30);
Draws the title string centered at x = width/2 (middle of canvas) and y = 30 (near top).
text("Click anywhere for a new remix!", width/2, 55);
Draws the instruction text centered at x = width/2 and y = 55, below the title.
fill(255);
Changes the text color to white (255) for the poem.
textSize(22);
Sets the font size to 22 pixels for the poem text (larger than the title).
textAlign(LEFT, TOP);
Aligns the poem text to the left horizontally and top vertically.
text(remixedPoem, margin, 90, width - margin * 2, height - 120);
Draws the remixed poem starting at position (margin, 90). The width parameter (width - margin * 2) constrains the text to wrap within the margins. The height parameter (height - 120) limits the text area vertically.

mousePressed()

mousePressed() is an event handler—a function that p5.js calls automatically in response to user input. Every time the user clicks, this function runs, creating a new remix. This is how the sketch becomes interactive.

// Click anywhere to generate a brand-new random tongue-twister remix
function mousePressed() {
  remixPoem();
}

đź”§ Subcomponents:

function-call Generate New Remix remixPoem();

Calls remixPoem() to shuffle words and display a new remix

Line by Line:

function mousePressed() {
mousePressed() is a special p5.js function that runs automatically whenever the user clicks the mouse anywhere on the canvas.
remixPoem();
Calls the remixPoem() function, which shuffles the words and triggers a redraw to display the new remix.

windowResized()

windowResized() ensures the sketch remains responsive when the user resizes their browser window. Without this function, the canvas would stay the same size and the text wouldn't reflow. This is important for making sketches work well on different screen sizes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  redraw(); // so the poem reflows to the new size
}

đź”§ Subcomponents:

responsive-design Resize Canvas to Window resizeCanvas(windowWidth, windowHeight);

Updates the canvas size to match the new window dimensions

function-call Redraw with New Layout redraw();

Triggers draw() to reflow the poem text to fit the new canvas size

Line by Line:

function windowResized() {
windowResized() is a special p5.js function that runs automatically whenever the browser window is resized.
resizeCanvas(windowWidth, windowHeight);
Updates the canvas dimensions to match the new window size. windowWidth and windowHeight are p5.js variables that update automatically when the window resizes.
redraw();
Calls draw() once to redisplay the poem with the new canvas dimensions, so the text reflows properly to the new width.

📦 Key Variables

poemLines array

Stores the lines of the poem loaded from poem.txt. Each element is one line of text.

let poemLines; // filled by loadStrings('poem.txt')
words array

Stores all individual words extracted from the poem. This array is shuffled each time a new remix is generated.

let words = []; // filled by processPoemIntoWords()
remixedPoem string

Stores the current remixed poem as a single formatted string with newlines every 8 words. This is what gets displayed on the canvas.

let remixedPoem = ''; // updated by remixPoem()
WORDS_PER_LINE number

A constant that controls how many words appear on each line of the remix (set to 8). Using a constant makes it easy to change the layout.

const WORDS_PER_LINE = 8;

đź§Ş Try This!

Experiment with the code by making these changes:

  1. Change WORDS_PER_LINE from 8 to 4 or 12 to see how the poem reformats with fewer or more words per line.
  2. Modify the background color in draw() from background(20, 25, 35) to background(50, 100, 150) to change the canvas color to a different shade of blue.
  3. Change the title text color from fill(150, 200, 255) to fill(255, 100, 100) to make the title red instead of light blue.
  4. Add console.log(words) at the end of processPoemIntoWords() to see all the individual words extracted from the poem in the browser console.
  5. Modify the text size for the poem from textSize(22) to textSize(16) or textSize(28) to make the poem smaller or larger.
Open in Editor & Experiment →

đź”§ Potential Improvements

Here are some ways this code could be enhanced:

BUG remixPoem() function

If the total number of words is not evenly divisible by WORDS_PER_LINE, the last line will have fewer words and no newline will be added after it. This is minor but inconsistent.

đź’ˇ After the loop, add: if (result.length > 0 && result[result.length - 1] !== '\n') { result += '\n'; } to ensure the poem always ends with a newline.

PERFORMANCE processPoemIntoWords() function

The function is called only once in setup(), but if you wanted to load different poems, recreating the words array repeatedly would be inefficient.

đź’ˇ Consider caching the processed words or creating a separate function to load and process poems, so you can easily swap poems without reprocessing.

STYLE Global variables

The variables poemLines, words, and remixedPoem are declared without initialization values (except remixedPoem = ''), making it unclear what type they should be.

đź’ˇ Add explicit initialization: let poemLines = []; let words = []; let remixedPoem = ''; This makes the code clearer and prevents potential undefined errors.

FEATURE mousePressed() function

The sketch only responds to mouse clicks. Some users might expect keyboard input or other interaction methods.

đź’ˇ Add a keyPressed() function to generate a new remix when the user presses the spacebar: function keyPressed() { if (key === ' ') remixPoem(); }

FEATURE remixPoem() function

There's no way to see the original poem—only the remixed versions. Users might want to compare the remix to the source.

đź’ˇ Add a toggle button or keyboard shortcut to display the original poem alongside the remix, or add a 'Show Original' option in the UI.

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, processpoemintowords, remixpoem, draw, mousepressed, windowresized
Code Flow Diagram