Sketch 2026-01-17 14:49

This sketch creates a smooth text transition animation that alternates between two phrases: 'Adra Keren' and 'Adra Anak Emas'. Each phrase fades in and holds for 2 seconds, then fades out over 1 second as the other phrase fades in, creating a continuous loop. The canvas resizes responsively to fit the window.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the text content — Edit phrase1 and phrase2 to display your own text instead of the names
  2. Speed up the animation cycle — Reduce holdDuration so each phrase displays for less time before fading
  3. Make the crossfade instant — Set transitionDuration to a very small number so phrases snap from one to the other instead of fading
  4. Make text red — Change the fill color from black (0) to red by using fill(255, 0, 0, alpha) instead of fill(0, alpha)
  5. Make text much larger — Divide by a smaller number in textSize() so the text scales to fill more of the screen
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch animates two text phrases that fade in and out in a continuous loop on a light gray background. The text alternates between 'Adra Keren' and 'Adra Anak Emas', with each phrase visible for 2 seconds before a 1-second crossfade brings in the next one. It teaches three powerful p5.js techniques: the millis() timer function that tracks elapsed time, alpha transparency in fill() that makes text fade smoothly, and the map() function that converts time ranges into opacity values.

The code is organized around a draw() function that runs 60 times per second, calculating where we are in the animation cycle using modulo arithmetic (the % operator). By studying it, you'll learn how professional text animations work—using time-based state machines to control visual properties like opacity. The sketch also demonstrates responsive design by using windowWidth and windowHeight to scale the text to any screen size.

⚙️ How It Works

  1. When the sketch loads, preload() fetches a custom Roboto font from a CDN, and setup() creates a canvas that fills the entire window and configures centered text at a responsive size.
  2. Every frame, draw() calculates how far through the animation cycle we are by taking the current milliseconds and using modulo to wrap time around: timer = millis() % cycleDuration.
  3. The code divides the cycle into four states: first phrase holds (2 seconds), crossfade transition (1 second), second phrase holds (2 seconds), then crossfade back (1 second).
  4. For each state, the code calculates the opacity (alpha value) of each phrase—either fully opaque (255), fully transparent (0), or somewhere between using map() and lerp() to smoothly interpolate during transitions.
  5. Both phrases are drawn at the exact same position (center of canvas) with their calculated alpha values, so they appear to fade in and out in place.
  6. The windowResized() function listens for window size changes and updates the canvas and text size to stay responsive.

🎓 Concepts You'll Learn

Time-based animationAlpha transparency and fill()millis() timer functionModulo operator for loopingmap() and lerp() for interpolationResponsive canvas sizingState machine logic

📝 Code Breakdown

preload()

preload() is special in p5.js—it runs before setup(), ensuring that slow resources like fonts and images are fully loaded before drawing begins. The URL pattern shows how to load fonts from Fontsource's free library.

function preload() {
  // Load the Roboto font from Fontsource CDN
  // Convert font name to slug: "Roboto" -> "roboto"
  // URL pattern: https://unpkg.com/@fontsource/{slug}@latest/files/{slug}-latin-400-normal.woff
  myFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
}
Line-by-line explanation (1 lines)
myFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
Loads a custom Roboto font from a CDN (content delivery network) and stores it in the myFont variable so it's ready before setup() runs

setup()

setup() runs once when the sketch starts. Here we prepare the canvas, load our font, and configure text appearance so draw() can focus on animation logic.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas

  // Set the loaded font and text properties
  textFont(myFont);
  textSize(min(width, height) / 10); // Responsive text size
  textAlign(CENTER, CENTER);
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window width and height, making the sketch responsive to any screen size
textFont(myFont);
Tells p5.js to use the Roboto font that was loaded in preload() for all text drawn after this line
textSize(min(width, height) / 10); // Responsive text size
Sets the text size to 1/10th of the smaller dimension (width or height)—this scales the text proportionally on any screen
textAlign(CENTER, CENTER);
Makes text align to its center point both horizontally and vertically, so text() will draw centered at the coordinates you give it

draw()

draw() runs 60 times per second. Here we calculate which state of the animation cycle we're in, compute the opacity for each phrase based on time, then draw both phrases at their calculated opacity. The magic is that both phrases are drawn at the same position—only their opacity changes, creating the illusion of one text transforming into another.

🔬 This State 0 block makes phrase1 fully opaque (255) and phrase2 invisible (0). What happens if you change alphaPhrase1 to 128 so phrase1 is only half-transparent during its display window? Will both phrases be visible at the same time?

  // State 0: Display Phrase 1 (Adra Keren)
  if (timer >= 0 && timer < holdDuration) {
    alphaPhrase1 = 255; // Fully opaque
    alphaPhrase2 = 0;   // Fully transparent
  }

🔬 These three lines create the smooth crossfade by using map() to create a 0-to-1 value, then lerp() to blend opacity. What happens if you swap the lerp() endpoints and do lerp(0, 255, t) for phrase1 and lerp(255, 0, t) for phrase2? Will the fade reverse direction?

    let t = map(timer, holdDuration, holdDuration + transitionDuration, 0, 1);
    alphaPhrase1 = lerp(255, 0, t); // Fade out phrase1
    alphaPhrase2 = lerp(0, 255, t);   // Fade in phrase2
function draw() {
  background(220); // Light gray background

  // Calculate the current time within the cycle
  let timer = millis() % cycleDuration;

  let alphaPhrase1 = 0; // Alpha (transparency) for phrase1
  let alphaPhrase2 = 0; // Alpha (transparency) for phrase2

  // --- Transition Logic ---
  // State 0: Display Phrase 1 (Adra Keren)
  if (timer >= 0 && timer < holdDuration) {
    alphaPhrase1 = 255; // Fully opaque
    alphaPhrase2 = 0;   // Fully transparent
  }
  // State 1: Transition from Phrase 1 to Phrase 2
  else if (timer >= holdDuration && timer < holdDuration + transitionDuration) {
    let t = map(timer, holdDuration, holdDuration + transitionDuration, 0, 1);
    alphaPhrase1 = lerp(255, 0, t); // Fade out phrase1
    alphaPhrase2 = lerp(0, 255, t);   // Fade in phrase2
  }
  // State 2: Display Phrase 2 (Adra Anak Emas)
  else if (timer >= holdDuration + transitionDuration && timer < holdDuration + transitionDuration + holdDuration) {
    alphaPhrase1 = 0;
    alphaPhrase2 = 255;
  }
  // State 3: Transition from Phrase 2 back to Phrase 1
  else { // timer >= holdDuration + transitionDuration + holdDuration && timer < cycleDuration
    let t = map(timer, holdDuration + transitionDuration + holdDuration, cycleDuration, 0, 1);
    alphaPhrase2 = lerp(255, 0, t); // Fade out phrase2
    alphaPhrase1 = lerp(0, 255, t);   // Fade in phrase1
  }

  // --- Draw the text ---
  push(); // Isolate drawing styles
  fill(0, alphaPhrase1); // Black text with calculated alpha
  text(phrase1, width / 2, height / 2); // Draw phrase1
  pop();

  push(); // Isolate drawing styles
  fill(0, alphaPhrase2); // Black text with calculated alpha
  text(phrase2, width / 2, height / 2); // Draw phrase2
  pop();
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

calculation Cycle Timer let timer = millis() % cycleDuration;

Wraps time around in a loop: every cycleDuration milliseconds, timer resets from 0 to cycleDuration, creating the repeating animation

conditional Phrase 1 Display if (timer >= 0 && timer < holdDuration) {

Detects when we're in the first 2 seconds of the cycle and shows only phrase1 at full opacity

conditional Crossfade Transition else if (timer >= holdDuration && timer < holdDuration + transitionDuration) {

Detects the 1-second fade window and smoothly blends opacity from phrase1 to phrase2

conditional Phrase 2 Display else if (timer >= holdDuration + transitionDuration && timer < holdDuration + transitionDuration + holdDuration) {

Detects when we're in the middle 2 seconds of the cycle and shows only phrase2 at full opacity

conditional Reverse Crossfade else { // timer >= holdDuration + transitionDuration + holdDuration && timer < cycleDuration

Final transition fades phrase2 out and phrase1 back in to complete the cycle

background(220); // Light gray background
Clears the canvas with light gray (RGB 220, 220, 220) every frame, erasing the previous text before drawing the new frame
let timer = millis() % cycleDuration;
Gets the current milliseconds since the sketch started, then uses % to wrap it within 0 to cycleDuration, creating an infinite loop
if (timer >= 0 && timer < holdDuration) {
If we're in the first 2000ms of the cycle, enter State 0 where phrase1 is fully visible
alphaPhrase1 = 255; // Fully opaque
Sets phrase1's opacity to maximum (255 is fully visible in p5.js fill with alpha)
alphaPhrase2 = 0; // Fully transparent
Sets phrase2's opacity to zero (0 is completely invisible)
else if (timer >= holdDuration && timer < holdDuration + transitionDuration) {
If we're between 2000ms and 3000ms, enter State 1 where the crossfade happens
let t = map(timer, holdDuration, holdDuration + transitionDuration, 0, 1);
Converts the timer (2000-3000 range) into a smooth 0-to-1 value: t goes from 0 at the start of transition to 1 at the end
alphaPhrase1 = lerp(255, 0, t); // Fade out phrase1
Uses lerp() to smoothly blend from 255 to 0 as t goes 0 to 1, making phrase1 fade out during the transition
alphaPhrase2 = lerp(0, 255, t); // Fade in phrase2
Uses lerp() to smoothly blend from 0 to 255 as t goes 0 to 1, making phrase2 fade in during the transition
else if (timer >= holdDuration + transitionDuration && timer < holdDuration + transitionDuration + holdDuration) {
If we're between 3000ms and 5000ms, enter State 2 where phrase2 is fully visible
alphaPhrase1 = 0;
Hides phrase1 completely during the second phrase display window
alphaPhrase2 = 255;
Shows phrase2 at full opacity during its 2-second display window
else { // timer >= holdDuration + transitionDuration + holdDuration && timer < cycleDuration
If we're between 5000ms and 6000ms (the final second of the cycle), enter State 3 where the reverse crossfade happens
let t = map(timer, holdDuration + transitionDuration + holdDuration, cycleDuration, 0, 1);
Converts the final transition time window (5000-6000ms) into a 0-to-1 value t
alphaPhrase2 = lerp(255, 0, t); // Fade out phrase2
Fades phrase2 out as we approach the end of the cycle
alphaPhrase1 = lerp(0, 255, t); // Fade in phrase1
Fades phrase1 back in so it's ready when the cycle loops and State 0 begins again
push(); // Isolate drawing styles
Creates a checkpoint for drawing settings—any changes we make (like fill color) won't affect code that comes later
fill(0, alphaPhrase1); // Black text with calculated alpha
Sets the fill color to black (0, 0, 0) with opacity equal to alphaPhrase1—text drawn after this will use this color and transparency
text(phrase1, width / 2, height / 2); // Draw phrase1
Draws phrase1 centered on the canvas at the calculated opacity (invisible during transitions, fully visible when alphaPhrase1 = 255)
pop();
Restores the drawing settings to before the last push(), so phrase2's fill color won't interfere
push(); // Isolate drawing styles
Creates another checkpoint for phrase2's drawing settings
fill(0, alphaPhrase2); // Black text with calculated alpha
Sets the fill color to black with opacity equal to alphaPhrase2—this is independent of phrase1's opacity
text(phrase2, width / 2, height / 2); // Draw phrase2
Draws phrase2 at the exact same position as phrase1, but at its own calculated opacity, creating the crossfade illusion
pop();
Restores the drawing settings after drawing phrase2

windowResized()

windowResized() is automatically called by p5.js whenever the browser window is resized. Without this function, the canvas would stay at its original size. This ensures the sketch always fills the entire visible window and text stays readable at any screen size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Re-calculate text size for responsiveness
  textSize(min(width, height) / 10);
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
When the browser window resizes, this updates the canvas to fill the new window dimensions instead of staying at its original size
textSize(min(width, height) / 10);
Recalculates the text size based on the new canvas dimensions so text scales proportionally—if window gets smaller, text shrinks; if it grows, text grows

📦 Key Variables

myFont p5.Font

Stores the loaded Roboto font so it can be applied to all text in the sketch

let myFont;
phrase1 string

The first text phrase to animate: 'Adra Keren'

let phrase1 = "Adra Keren";
phrase2 string

The second text phrase to animate: 'Adra Anak Emas'

let phrase2 = "Adra Anak Emas";
holdDuration number

How long each phrase stays fully visible before fading (in milliseconds)—set to 2000ms

const holdDuration = 2000;
transitionDuration number

How long the crossfade between phrases takes (in milliseconds)—set to 1000ms

const transitionDuration = 1000;
cycleDuration number

Total time for one complete animation cycle (hold1 + fade + hold2 + fade back)—calculated as 6000ms

const cycleDuration = (holdDuration + transitionDuration) * 2;
timer number

The current position within the animation cycle (0 to cycleDuration), calculated fresh each frame using millis() and modulo

let timer = millis() % cycleDuration;
alphaPhrase1 number

The opacity value (0-255) for phrase1 in the current frame—updated each frame based on which state of the cycle we're in

let alphaPhrase1 = 0;
alphaPhrase2 number

The opacity value (0-255) for phrase2 in the current frame—updated each frame based on which state of the cycle we're in

let alphaPhrase2 = 0;
t number

A normalized 0-to-1 transition value used in crossfade states to smoothly interpolate between opacity values

let t = map(timer, holdDuration, holdDuration + transitionDuration, 0, 1);

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

STYLE draw() transition logic

The four state conditions are repetitive and use hardcoded offset calculations (holdDuration + transitionDuration, etc.) making them hard to read and maintain

💡 Define state-start times as constants (const STATE1_START = holdDuration, const STATE2_START = holdDuration + transitionDuration) to make conditions clearer and DRY (Don't Repeat Yourself)

PERFORMANCE setup() and windowResized()

The same textSize calculation (min(width, height) / 10) is duplicated in both functions

💡 Create a helper function like function updateTextSize() { textSize(min(width, height) / 10); } and call it from both places, reducing code duplication and making future changes easier

FEATURE global scope

The sketch only animates two phrases in a fixed sequence; adding more phrases or changing the animation order would require extensive refactoring

💡 Store phrases in an array like const phrases = ["Adra Keren", "Adra Anak Emas", "More Text"] and loop through them with an index variable, making it easy to add or reorder phrases without changing state logic

BUG draw() text rendering

Both phrases are drawn at the exact same coordinates; if both have non-zero alpha simultaneously (during manual edits), they could create confusing visual overlap

💡 Add comments clarifying that phrases should never both be fully visible, or implement a safety check to ensure alphaPhrase1 + alphaPhrase2 never exceeds 255 during crossfades

🔄 Code Flow

Code flow showing preload, setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> timercalculation[timer-calculation] draw --> state0phrase1[state0-phrase1] draw --> state1transition[state1-transition] draw --> state2phrase2[state2-phrase2] draw --> state3transition[state3-reverse-transition] timercalculation -->|Calculate time| draw state0phrase1 -->|Check time in first 2s| draw state1transition -->|Check time in 1s fade| draw state2phrase2 -->|Check time in middle 2s| draw state3transition -->|Check time in last 1s fade| draw click setup href "#fn-setup" click draw href "#fn-draw" click timercalculation href "#sub-timer-calculation" click state0phrase1 href "#sub-state0-phrase1" click state1transition href "#sub-state1-transition" click state2phrase2 href "#sub-state2-phrase2" click state3transition href "#sub-state3-reverse-transition"

❓ Frequently Asked Questions

What visual experience does the p5.js sketch 'Sketch 2026-01-17 14:49' create?

This sketch visually transitions between two phrases, 'Adra Keren' and 'Adra Anak Emas', using smooth fade-in and fade-out effects against a light gray background.

Is there any user interaction in this p5.js sketch?

The sketch is not interactive; it runs automatically, displaying the phrases in a continuous loop.

What creative coding technique is demonstrated in this sketch?

The sketch showcases the use of alpha blending and timed transitions to create a visually appealing text animation.

Preview

Sketch 2026-01-17 14:49 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-01-17 14:49 - Code flow showing preload, setup, draw, windowresized
Code Flow Diagram