Teachable machine > Web Serial

This sketch creates an interactive onboarding experience with animated floating particles, auto-advancing instruction panels, and smooth fade transitions. Users can tap to skip through 10 instructional steps that introduce the p5js.ai platform, with visual feedback through pulsing badges, progress dots, and responsive typography.

๐ŸŽ“ Concepts You'll Learn

Animation loopParticle systemColor modes (HSB)Responsive canvasConditional renderingArray iterationModulo operatorFade transitionsProgress trackingEvent handling

๐Ÿ”„ Code Flow

Code flow showing preload, setup, 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 --> particle-animation-loop[Particle Animation Loop] draw --> progress-dots-loop[Progress Indicator Dots] draw --> step-advancement-condition[Auto-Advance Step Logic] draw --> fade-calculation[Alpha Fade Transition] draw --> pulse-animation[Pulse Animation] particle-animation-loop --> particle-creation-loop[Particle Initialization Loop] particle-animation-loop --> particle-reset-condition[Particle Reset Check] particle-creation-loop --> particle-animation-loop particle-animation-loop --> particle-reset-condition click setup href "#fn-setup" click draw href "#fn-draw" click particle-animation-loop href "#sub-particle-animation-loop" click particle-creation-loop href "#sub-particle-creation-loop" click particle-reset-condition href "#sub-particle-reset-condition" click step-advancement-condition href "#sub-step-advancement-condition" click fade-calculation href "#sub-fade-calculation" click pulse-animation href "#sub-pulse-animation" click progress-dots-loop href "#sub-progress-dots-loop"

๐Ÿ“ Code Breakdown

preload()

preload() runs before setup() and is essential for loading external assets like fonts, images, and sounds. Without it, these resources might not be available when you try to use them.

function preload() {
  robotoFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
}

Line by Line:

robotoFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff')
Loads the Roboto font from a CDN before setup() runs. preload() ensures fonts are ready before drawing begins, preventing text rendering issues.

setup()

setup() runs once when the sketch starts. It's where you initialize your canvas, set drawing modes, and create starting objects. The particle system here creates a subtle animated background.

function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont(robotoFont);
  colorMode(HSB, 360, 100, 100, 100);
  noStroke();
  
  // Create floating particles for subtle background animation
  for (let i = 0; i < 25; i++) {
    particles.push({
      x: random(width),
      y: random(height),
      size: random(4, 10),
      speed: random(0.2, 0.8),
      hue: random(360)
    });
  }
}

๐Ÿ”ง Subcomponents:

for-loop Particle Initialization Loop for (let i = 0; i < 25; i++) { particles.push({...}) }

Creates 25 particle objects with random positions, sizes, speeds, and colors, storing them in the particles array

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window, making the sketch responsive to window size
textFont(robotoFont)
Sets the font for all text to the Roboto font that was loaded in preload()
colorMode(HSB, 360, 100, 100, 100)
Switches from RGB to HSB (Hue, Saturation, Brightness) color mode with ranges 0-360 for hue and 0-100 for saturation/brightness/alpha. This makes color animations easier
noStroke()
Removes outlines from all shapes, so only filled shapes will be drawn
for (let i = 0; i < 25; i++)
Loops 25 times to create 25 particles
particles.push({x: random(width), y: random(height), size: random(4, 10), speed: random(0.2, 0.8), hue: random(360)})
Creates a particle object with random x/y position, size between 4-10 pixels, speed between 0.2-0.8 pixels per frame, and random hue color, then adds it to the particles array

draw()

draw() runs 60 times per second, creating smooth animation. This sketch combines particle animation, auto-advancing timers, fade transitions, and responsive text sizing to create an engaging onboarding experience. The use of progress-based calculations (like the fade alpha) creates smooth, professional-looking transitions.

function draw() {
  // Dark gradient background
  background(240, 25, 10);
  
  // Animate floating particles
  for (let p of particles) {
    p.y -= p.speed;
    if (p.y < -20) {
      p.y = height + 20;
      p.x = random(width);
    }
    p.hue = (p.hue + 0.3) % 360;
    fill(p.hue, 50, 70, 15);
    circle(p.x, p.y, p.size);
  }
  
  // Auto-advance steps
  stepTimer++;
  if (stepTimer >= stepDuration) {
    stepTimer = 0;
    step = (step + 1) % instructions.length;
  }
  
  let current = instructions[step];
  let progress = stepTimer / stepDuration;
  
  // Smooth fade in/out
  let alpha = progress < 0.15 ? map(progress, 0, 0.15, 0, 100) :
              progress > 0.85 ? map(progress, 0.85, 1, 100, 0) : 100;
  
  let centerY = height / 2;
  
  // Animated step number with gentle pulse
  let pulse = 1 + sin(frameCount * 0.08) * 0.08;
  let iconSize = 50 * pulse;
  
  // Draw circular badge with step number
  fill(280, 60, 90, alpha * 0.9);
  circle(width / 2, centerY - 50, iconSize);
  
  // Step number inside circle
  fill(0, 0, 100, alpha);
  textSize(24 * pulse);
  textAlign(CENTER, CENTER);
  text(step + 1, width / 2, centerY - 50);
  
  // Title - responsive size
  textSize(min(26, width / 15));
  fill(280, 70, 100, alpha);
  text(current.title, width / 2, centerY + 15);
  
  // Description - responsive size
  textSize(min(16, width / 24));
  fill(0, 0, 75, alpha * 0.85);
  text(current.desc, width / 2, centerY + 50);
  
  // Progress indicator dots
  let dotSpacing = 14;
  let dotsWidth = (instructions.length - 1) * dotSpacing;
  let dotsX = (width - dotsWidth) / 2;
  
  for (let i = 0; i < instructions.length; i++) {
    let isActive = i === step;
    fill(isActive ? color(280, 70, 100) : color(0, 0, 30));
    circle(dotsX + i * dotSpacing, height - 35, isActive ? 7 : 4);
  }
  
  // Swipe hint
  textSize(11);
  fill(0, 0, 45, 50);
  text("Tap to skip ยท Swipe panels to navigate", width / 2, height - 60);
}

๐Ÿ”ง Subcomponents:

for-loop Particle Animation Loop for (let p of particles) { p.y -= p.speed; ... circle(p.x, p.y, p.size); }

Updates each particle's position, color, and draws it on screen each frame

conditional Particle Reset Check if (p.y < -20) { p.y = height + 20; p.x = random(width); }

Recycles particles that float off the top by moving them to the bottom with a new random x position

conditional Auto-Advance Step Logic if (stepTimer >= stepDuration) { stepTimer = 0; step = (step + 1) % instructions.length; }

Automatically advances to the next instruction panel when the timer reaches the duration

calculation Alpha Fade Transition let alpha = progress < 0.15 ? map(progress, 0, 0.15, 0, 100) : progress > 0.85 ? map(progress, 0.85, 1, 100, 0) : 100;

Calculates opacity for smooth fade-in at start (0-15%) and fade-out at end (85-100%) of each step

calculation Pulse Animation let pulse = 1 + sin(frameCount * 0.08) * 0.08;

Creates a gentle pulsing effect using sine wave that oscillates between 0.92 and 1.08

for-loop Progress Indicator Dots for (let i = 0; i < instructions.length; i++) { ... circle(dotsX + i * dotSpacing, height - 35, isActive ? 7 : 4); }

Draws dots at the bottom showing which step is active (larger) and which are inactive (smaller)

Line by Line:

background(240, 25, 10)
Fills the canvas with a dark color (hue 240=blue, low saturation and brightness) each frame, clearing previous drawings
for (let p of particles)
Loops through each particle object in the particles array using a for-of loop
p.y -= p.speed
Moves each particle upward by subtracting its speed from its y position (lower y = higher on screen)
if (p.y < -20)
Checks if the particle has moved above the canvas (off-screen)
p.y = height + 20
Resets the particle to the bottom of the canvas, creating a looping effect
p.x = random(width)
Gives the recycled particle a new random horizontal position
p.hue = (p.hue + 0.3) % 360
Slowly shifts the particle's color by adding 0.3 each frame, using modulo (%) to wrap back to 0 after 360
fill(p.hue, 50, 70, 15)
Sets the fill color using HSB mode with the particle's hue, 50% saturation, 70% brightness, and 15% opacity (very transparent)
circle(p.x, p.y, p.size)
Draws the particle as a circle at its current position with its size
stepTimer++
Increments the step timer by 1 each frame to track how long the current step has been displayed
if (stepTimer >= stepDuration)
Checks if enough frames have passed to advance to the next instruction
step = (step + 1) % instructions.length
Advances to the next step, using modulo to loop back to 0 after the last instruction
let progress = stepTimer / stepDuration
Calculates progress as a decimal from 0 to 1 (0% to 100% through the current step)
let alpha = progress < 0.15 ? map(progress, 0, 0.15, 0, 100) : progress > 0.85 ? map(progress, 0.85, 1, 100, 0) : 100
Creates a fade effect: fades in during first 15%, stays fully visible in middle, fades out in last 15%
let pulse = 1 + sin(frameCount * 0.08) * 0.08
Uses sine wave to create a gentle pulsing animation that smoothly oscillates between 0.92 and 1.08
let iconSize = 50 * pulse
Multiplies the base size by the pulse value so the circle grows and shrinks smoothly
fill(280, 60, 90, alpha * 0.9)
Sets fill color to purple (hue 280) with slightly reduced opacity to make it subtle
circle(width / 2, centerY - 50, iconSize)
Draws the pulsing circular badge centered horizontally and 50 pixels above the vertical center
text(step + 1, width / 2, centerY - 50)
Displays the step number (1-10) inside the circle, centered at the same position
textSize(min(26, width / 15))
Sets title text size to 26 pixels or smaller if the window is narrow, ensuring it fits on mobile
text(current.title, width / 2, centerY + 15)
Displays the current instruction's title centered horizontally, 15 pixels below center
text(current.desc, width / 2, centerY + 50)
Displays the current instruction's description centered horizontally, 50 pixels below center
let dotSpacing = 14
Sets the horizontal distance between progress indicator dots to 14 pixels
let dotsWidth = (instructions.length - 1) * dotSpacing
Calculates total width needed for all dots (9 gaps ร— 14 pixels = 126 pixels for 10 dots)
let dotsX = (width - dotsWidth) / 2
Calculates the starting x position to center all the dots horizontally
for (let i = 0; i < instructions.length; i++)
Loops through each instruction to draw a progress dot for it
let isActive = i === step
Checks if this dot represents the current step
fill(isActive ? color(280, 70, 100) : color(0, 0, 30))
Colors the dot bright purple if active, dark gray if inactive
circle(dotsX + i * dotSpacing, height - 35, isActive ? 7 : 4)
Draws the dot at the calculated position, making it larger (7px) if active or smaller (4px) if inactive
text("Tap to skip ยท Swipe panels to navigate", width / 2, height - 60)
Displays a hint at the bottom telling users how to interact with the sketch

mousePressed()

mousePressed() is called whenever the user clicks or taps. This sketch uses it to let users skip ahead through instructions manually, overriding the automatic advancement.

function mousePressed() {
  step = (step + 1) % instructions.length;
  stepTimer = 0;
}

Line by Line:

step = (step + 1) % instructions.length
Advances to the next instruction step, looping back to 0 after the last one using modulo operator
stepTimer = 0
Resets the timer to 0 so the new step starts fresh without waiting

windowResized()

windowResized() is called automatically whenever the user resizes their browser window. This keeps the sketch responsive and fullscreen on any device size. Without this, the canvas would stay at its original size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Resizes the canvas to match the new window dimensions whenever the browser window is resized

๐Ÿ“ฆ Key Variables

robotoFont object

Stores the loaded Roboto font object, used for all text rendering in the sketch

let robotoFont;
particles array

Array that stores all 25 particle objects, each with x, y, size, speed, and hue properties for the animated background

let particles = [];
step number

Tracks which instruction (0-9) is currently being displayed

let step = 0;
stepTimer number

Counts frames elapsed in the current step, used to auto-advance when it reaches stepDuration

let stepTimer = 0;
stepDuration number

Number of frames each instruction should display before auto-advancing (225 frames โ‰ˆ 3.75 seconds at 60 fps)

let stepDuration = 225;
instructions array

Array of 10 objects, each containing a title and description for the onboarding steps

let instructions = [{title: "Welcome to p5js AI!", desc: "Swipe between panels to explore"}, ...];

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change stepDuration from 225 to 100 to make the instructions advance much faster, or to 500 to give users more time to read each step
  2. Modify the particle count from 25 to 50 in the setup() loop to create a denser animated background, or reduce to 10 for a sparser effect
  3. Change the pulse animation on line 'let pulse = 1 + sin(frameCount * 0.08) * 0.08' to 'let pulse = 1 + sin(frameCount * 0.15) * 0.15' to make the circle pulse faster and more dramatically
  4. Modify the fade timing by changing 'progress < 0.15' to 'progress < 0.3' to make the fade-in last longer (30% of the step instead of 15%)
  5. Change the particle color on line 'fill(p.hue, 50, 70, 15)' to 'fill(p.hue, 80, 90, 30)' to make particles more saturated, brighter, and more visible
  6. Add a new instruction object to the instructions array with your own title and description to extend the onboarding flow
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

BUG draw() - particle animation

Particles that wrap around can have visible jumps if they're large and positioned near the edge. The check 'if (p.y < -20)' doesn't account for particle size.

๐Ÿ’ก Change the condition to 'if (p.y < -p.size)' to account for the particle's radius, ensuring it's fully off-screen before recycling

PERFORMANCE draw() - progress dots loop

The color() function is called twice per dot in every frame, creating unnecessary color objects. This is called 10 times per frame (600 times per second).

๐Ÿ’ก Pre-calculate the colors outside the loop: 'let activeColor = color(280, 70, 100); let inactiveColor = color(0, 0, 30);' then use them in the loop

STYLE setup() - particle initialization

The particle object creation is verbose and could be more maintainable with a helper function

๐Ÿ’ก Create a helper function like 'function createParticle() { return {x: random(width), y: random(height), size: random(4, 10), speed: random(0.2, 0.8), hue: random(360)}; }' and call it in the loop

FEATURE mousePressed()

The sketch mentions 'Swipe panels to navigate' in the hint text, but only tap/click is implemented. Swipe functionality is missing.

๐Ÿ’ก Implement touch swipe detection using touchStartX and touchEndX variables, calculating swipe direction to advance or go back through instructions

BUG draw() - alpha calculation

The ternary operator for alpha is hard to read and could fail if progress values are exactly on boundaries due to floating-point precision

๐Ÿ’ก Use a helper function or constrain() to make this more robust: 'let alpha = progress < 0.15 ? map(progress, 0, 0.15, 0, 100) : progress > 0.85 ? map(progress, 0.85, 1, 100, 0) : 100;' could be refactored into a separate function

Preview

Teachable machine > Web Serial - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Teachable machine > Web Serial - Code flow showing preload, setup, draw, mousepressed, windowresized
Code Flow Diagram