triple t virus

This sketch creates a fake virus warning screen with a menacing red background, animated text that floats and fades, two interactive buttons, and terrifying jumpscare images that flash on screen at unpredictable intervals. It combines visual pranks, sound effects, and responsive design to create an immersive (and unsettling) interactive experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make jumpscares appear every 3 seconds — The first jumpscare will appear much more frequently and feel less predictable, ramping up the creep factor.
  2. Make the jumpscare stay visible longer — The image will linger on screen longer, creating more time for you to feel scared before it disappears.
  3. Change the background from red to black — Instantly changes the mood from a virus alert (red) to something darker and more ominous (black).
  4. Make floating text fade but stay longer — The text animation will last 4 seconds instead of 2, giving a slower, more eerie fade-out effect.
  5. Make the static noise louder — The background white noise will be much louder, adding to the unsettling atmosphere.
  6. Change the Send Money button message — When you click the button, a custom insult floats up instead of the default one.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates a malicious virus warning screen, complete with a blood-red background, ominous text, two clickable buttons, and two different jumpscare images that suddenly appear at random intervals to startle you. It demonstrates several essential p5.js techniques: loading external images and fonts, managing animation timers, creating custom animation classes, handling user interaction through buttons, responding to window resizing, and playing background audio using p5.sound.

The code is organized around the FloatingText class—a reusable object that animates text upward while fading it out—and two independent jumpscare timers that trigger at different intervals. By studying this sketch, you will learn how to schedule events in real time, layer multiple animations on top of each other, make your sketches responsive to window size changes, and structure code using classes to keep animations modular and easy to extend.

⚙️ How It Works

  1. When the sketch loads, preload() fetches two jumpscare images and a custom font from the internet, while setup() creates a full-screen canvas, positions two buttons, starts a white noise generator, and initializes two separate timers for the two jumpscares.
  2. Every frame, draw() paints the red background and the ominous text, then checks both jumpscare timers—if 13 seconds have passed, it displays the first jumpscare image for 0.5 seconds; if 5 seconds have passed (on a separate timer), it displays the second one.
  3. When you click either button, the click handler creates a FloatingText object and pushes it into the floatingTexts array—this object animates upward and fades out over 2 seconds.
  4. The draw loop continuously updates and displays all active FloatingText objects, removing them from the array once their animation finishes.
  5. If you resize the browser window, windowResized() recalculates all text sizes and button positions so the layout stays balanced at any screen width.

🎓 Concepts You'll Learn

Timer-based event schedulingImage preloading and renderingCustom animation classArray manipulation with spliceResponsive canvas resizingMouse interaction and button handlingFade-out animation with alphaText alignment and positioning

📝 Code Breakdown

preload()

preload() is a special p5.js function that runs before setup()—it's the only place where you can safely call blocking operations like loadImage() and loadFont(). If you load images inside setup() or draw(), your sketch will stutter.

function preload() {
  // Load the first jumpscare image
  jumpscareImage = loadImage('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ5y9hqK1_1m7460gHkxhPzZGlqL53u1SiUwA&s');
  
  // Load the second jumpscare image (NEW)
  jumpscareImage2 = loadImage('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR8cVYRFVxnhmOEBzEkBjpMrhYG7LuQWj8MVA&s');
  
  // Load a suitable font for the virus text to give it a techy, monospaced look.
  // We're using Roboto Mono from the Fontsource CDN, which is reliable and works well with p5.js loadFont().
  // This specific URL is for latin characters, 400 weight (normal), normal style, in woff format.
  virusFont = loadFont('https://cdn.jsdelivr.net/npm/@fontsource/roboto-mono@latest/files/roboto-mono-latin-400-normal.woff');
}
Line-by-line explanation (3 lines)
jumpscareImage = loadImage('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ5y9hqK1_1m7460gHkxhPzZGlqL53u1SiUwA&s');
Loads the first jumpscare image from a URL and stores it in the jumpscareImage variable—p5.js blocks the rest of the sketch until this finishes.
jumpscareImage2 = loadImage('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR8cVYRFVxnhmOEBzEkBjpMrhYG7LuQWj8MVA&s');
Loads the second jumpscare image from a URL and stores it in jumpscareImage2—having two different images makes the scares feel less predictable.
virusFont = loadFont('https://cdn.jsdelivr.net/npm/@fontsource/roboto-mono@latest/files/roboto-mono-latin-400-normal.woff');
Loads the Roboto Mono font from a CDN so all text in the sketch has a technical, monospaced appearance that fits the virus warning aesthetic.

setup()

setup() runs once at the start of your sketch. Use it to initialize the canvas, load resources, create buttons, and set up any state you need. Everything here prepares the stage for draw() to run every frame.

function setup() {
  // Create a canvas that fills the entire browser window
  createCanvas(windowWidth, windowHeight);
  
  // Set the frame rate for smoother animations
  frameRate(60); 

  // Set the loaded font for all text drawn on the canvas
  textFont(virusFont);
  // Center all text horizontally and vertically
  textAlign(CENTER, CENTER);
  
  // Initialize jumpscare timers to the current time
  jumpscareTimer = millis();
  jumpscareTimer2 = millis(); // Initialize the second jumpscare timer (NEW)
  
  // Create the "Send Money" button
  virusButton = createButton('Send Money');
  // Create the new "Unlock Now" button
  unlockButton = createButton('Unlock Now');
  
  // Button styling (unified function to avoid repetition)
  function applyButtonStyle(button) {
    button.style('background-color', '#ff0000'); // Bright red background
    button.style('color', '#000000');           // Black text for high contrast
    button.style('font-family', 'Roboto Mono, monospace'); // Use the same techy font
    button.style('font-size', '24px');          // Large font size
    button.style('padding', '15px 30px');      // Generous padding for a bigger button
    button.style('border', '4px solid black');  // Thick black border for emphasis
    button.style('border-radius', '5px');       // Slightly rounded corners
    button.style('cursor', 'pointer');         // Change cursor to indicate it's clickable
  }

  applyButtonStyle(virusButton);
  applyButtonStyle(unlockButton);
  
  // Adjust position of both buttons
  positionButtons();
  
  // Attach click handlers to both buttons
  virusButton.mousePressed(handleButtonClick);
  unlockButton.mousePressed(handleUnlockButtonClick);
  
  // Adjust text sizes based on the initial window dimensions for responsiveness
  updateTextSizes();

  // --- Static Sound Setup (using p5.sound) ---
  // Create a white noise generator
  noiseGen = new p5.Noise('white');
  // Set a low amplitude for the background static (0.1 = 10% volume)
  noiseGen.amp(0.1);
  // Start the noise generator
  noiseGen.start();
  // Call userStartAudio() to enable audio playback in browsers
  // This needs to be called after a user interaction with the page.
  // Since the buttons are user interactions, the noise will start when the user clicks either of them,
  // or generally, when they interact with the sketch.
  userStartAudio(); 
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

function applyButtonStyle() nested helper function applyButtonStyle(button) { ... }

Applies consistent red-and-black styling to both buttons without repeating code twice

calculation White noise initialization noiseGen = new p5.Noise('white');

Creates a white noise generator that will play creepy static sound in the background

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window—this makes the virus warning feel immersive and takes over the whole screen.
frameRate(60);
Sets the sketch to run at 60 frames per second, which is fast enough for smooth animations and jumpscare timing.
textFont(virusFont);
Sets the font for all text drawn after this line to the Roboto Mono font that was loaded in preload().
textAlign(CENTER, CENTER);
Centers all text both horizontally and vertically around the coordinates you provide—this makes positioning text much easier.
jumpscareTimer = millis();
Records the current time in milliseconds—this becomes the starting point for timing the first jumpscare.
jumpscareTimer2 = millis();
Records the current time for the second jumpscare timer separately—having two independent timers lets the jumps scares appear at different intervals.
virusButton = createButton('Send Money');
Creates the first HTML button with the text 'Send Money' and stores it so we can position and style it later.
unlockButton = createButton('Unlock Now');
Creates the second HTML button with the text 'Unlock Now'—it will trigger a different message when clicked.
applyButtonStyle(virusButton);
Applies the red-and-black styling to the Send Money button by calling the helper function.
virusButton.mousePressed(handleButtonClick);
Tells p5.js to call the handleButtonClick() function whenever someone clicks the Send Money button.
noiseGen = new p5.Noise('white');
Creates a white noise generator object from the p5.sound library—this will be the eerie static sound.
noiseGen.start();
Starts playing the white noise in the background—it will continue looping until the sketch ends.

draw()

draw() runs 60 times per second and is the heartbeat of your animation. Every frame, it clears the canvas, redraws the static elements (background and text), checks timers, displays images, and animates floating text. The order of these operations matters—background clears the screen, then everything else draws on top of it.

🔬 This block triggers the first jumpscare every 13 seconds. What happens if you change jumpscareInterval to 3000 (3 seconds) or 30000 (30 seconds)? How does timing affect the scare factor?

  // Check if 13 seconds have passed since the last jumpscare
  if (millis() - jumpscareTimer > jumpscareInterval) {
    showJumpscare = true; // Set flag to display jumpscare image
    jumpscareStartTime = millis(); // Record when the jumpscare started
    jumpscareTimer = millis(); // Reset the timer for the next jumpscare
  }

🔬 This loop updates every floating text in the array. What happens if you comment out the floatingTexts.splice() line so texts are never removed? (Hint: it will grow slower, but the array never empties.)

  for (let i = floatingTexts.length - 1; i >= 0; i--) {
    floatingTexts[i].update();
    floatingTexts[i].display();
    if (!floatingTexts[i].active) {
      floatingTexts.splice(i, 1); // Remove inactive texts from the array
    }
  }
function draw() {
  background(255, 0, 0); // Draw the background as solid red
  
  // Draw the "triple t virus" text at the top
  fill(0); // Set text color to black
  textSize(virusTextSize); // Use the responsive text size
  text('triple t virus', width / 2, height * 0.2); // Position 20% down the screen
  
  // Draw the "Send 100000 dollars to unlock computer" text below the title
  fill(0); // Set text color to black
  textSize(virusInstructionSize); // Use the responsive instruction text size
  text('Send 100000 dollars to unlock computer', width / 2, height * 0.4); // Position 40% down the screen
  
  // --- Jumpscare Logic 1 (13 seconds) ---
  // Check if 13 seconds have passed since the last jumpscare
  if (millis() - jumpscareTimer > jumpscareInterval) {
    showJumpscare = true; // Set flag to display jumpscare image
    jumpscareStartTime = millis(); // Record when the jumpscare started
    jumpscareTimer = millis(); // Reset the timer for the next jumpscare
  }
  
  // If showJumpscare is true, draw the image
  if (showJumpscare) {
    imageMode(CENTER); // Draw images from their center point
    // Draw the jumpscare image centered on the canvas and scaled to cover it
    image(jumpscareImage, width / 2, height / 2, width, height);
    
    // After the jumpscare duration, hide the image
    if (millis() - jumpscareStartTime > jumpscareDuration) {
      showJumpscare = false;
    }
  }

  // --- Jumpscare Logic 2 (5 seconds - NEW) ---
  // Check if 5 seconds have passed since the last jumpscare
  if (millis() - jumpscareTimer2 > jumpscareInterval2) {
    showJumpscare2 = true; // Set flag to display the second jumpscare image
    jumpscareStartTime2 = millis(); // Record when the second jumpscare started
    jumpscareTimer2 = millis(); // Reset the timer for the next second jumpscare
  }
  
  // If showJumpscare2 is true, draw the second image
  if (showJumpscare2) {
    imageMode(CENTER); // Draw images from their center point
    // Draw the second jumpscare image centered on the canvas and scaled to cover it
    image(jumpscareImage2, width / 2, height / 2, width, height);
    
    // After the second jumpscare duration, hide the image
    if (millis() - jumpscareStartTime2 > jumpscareDuration2) {
      showJumpscare2 = false;
    }
  }

  // --- Floating Text Logic ---
  // Loop through all floating texts, update them, display them, and remove inactive ones
  for (let i = floatingTexts.length - 1; i >= 0; i--) {
    floatingTexts[i].update();
    floatingTexts[i].display();
    if (!floatingTexts[i].active) {
      floatingTexts.splice(i, 1); // Remove inactive texts from the array
    }
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

conditional First jumpscare trigger if (millis() - jumpscareTimer > jumpscareInterval) {

Checks if enough time has passed since the last jumpscare to trigger a new one at 13-second intervals

conditional First jumpscare rendering if (showJumpscare) {

Draws the first jumpscare image and removes it after 0.5 seconds

conditional Second jumpscare trigger if (millis() - jumpscareTimer2 > jumpscareInterval2) {

Checks if enough time has passed to trigger the second jumpscare at 5-second intervals

for-loop Floating text animation loop for (let i = floatingTexts.length - 1; i >= 0; i--) {

Updates and displays all active floating text animations, removing them when they finish

background(255, 0, 0);
Paints the entire canvas bright red every frame—this clears the previous frame and maintains the menacing background.
text('triple t virus', width / 2, height * 0.2);
Draws the title text centered horizontally and 20% down from the top—this is the first thing viewers see.
text('Send 100000 dollars to unlock computer', width / 2, height * 0.4);
Draws the ransom message centered and 40% down the screen—this is where the buttons will be positioned below it.
if (millis() - jumpscareTimer > jumpscareInterval) {
Subtracts the timer's last recorded time from the current time in milliseconds—if the difference is larger than 13,000 (13 seconds), it's time for a jumpscare.
showJumpscare = true;
Sets the flag to true so the next if-statement knows to draw the jumpscare image this frame.
jumpscareStartTime = millis();
Records the current time so we can measure how long the jumpscare has been visible and hide it after 0.5 seconds.
jumpscareTimer = millis();
Resets the timer so the next jumpscare will appear 13 seconds from now.
image(jumpscareImage, width / 2, height / 2, width, height);
Draws the jumpscare image centered on the canvas and scaled to fill the entire screen—this covers everything temporarily.
if (millis() - jumpscareStartTime > jumpscareDuration) {
Checks if the jumpscare has been visible for longer than 0.5 seconds (500 milliseconds).
showJumpscare = false;
Hides the jumpscare image by setting the flag to false—the screen goes back to red and text.
for (let i = floatingTexts.length - 1; i >= 0; i--) {
Loops backward through the floatingTexts array—looping backward is important because we'll remove items as we go, and backward prevents skipping elements.
floatingTexts[i].update();
Calls the update() method on each FloatingText object, which moves it upward and fades it out.
floatingTexts[i].display();
Calls the display() method on each FloatingText object to draw it at its current position and opacity.
floatingTexts.splice(i, 1);
Removes the FloatingText from the array once its animation finishes (when active becomes false)—this prevents the array from growing infinitely.

FloatingText (class)

Classes are blueprints for objects. Every time you click a button, a new FloatingText object is created with its own text, position, and animation timing. The update() method runs every frame to move and fade the text, while display() draws it. When the animation finishes, active becomes false, and the draw loop removes it from the array.

🔬 These three lines handle movement and fading. What happens if you comment out the this.alpha lines so the text moves but never fades? Does it feel less polished?

    this.y += this.speedY; // Move text upwards
    this.alpha = map(progress, 0, 1, 255, 0); // Fade out text as it moves
    this.alpha = constrain(this.alpha, 0, 255); // Ensure alpha stays within bounds
class FloatingText {
  constructor(text, x, y, duration, speedY, fontSize, fontColor = 0) {
    this.text = text;
    this.x = x;
    this.y = y;
    this.duration = duration; // milliseconds for the animation
    this.speedY = speedY;     // pixels per frame the text moves up (negative for up)
    this.fontSize = fontSize;
    this.fontColor = fontColor;
    this.alpha = 255;         // Initial opacity
    this.startTime = millis(); // Time when the text started floating
    this.active = true;       // Flag to indicate if the text is still animating
  }

  update() {
    if (!this.active) return; // If not active, no need to update

    let elapsedTime = millis() - this.startTime; // Time passed since start
    let progress = elapsedTime / this.duration;   // Animation progress (0 to 1)

    if (progress >= 1) {
      this.active = false; // Animation finished
      return;
    }

    this.y += this.speedY; // Move text upwards
    this.alpha = map(progress, 0, 1, 255, 0); // Fade out text as it moves
    this.alpha = constrain(this.alpha, 0, 255); // Ensure alpha stays within bounds
  }

  display() {
    if (!this.active) return; // If not active, no need to display

    fill(this.fontColor, this.alpha); // Set text color with current opacity
    textSize(this.fontSize);         // Set text size
    text(this.text, this.x, this.y); // Draw the text at its current position
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

function constructor() - initializer constructor(text, x, y, duration, speedY, fontSize, fontColor = 0) {

Stores all the properties needed for this individual floating text animation (position, duration, speed, color)

function update() - animation logic update() {

Calculates elapsed time, moves text upward, and fades out the opacity each frame

function display() - rendering display() {

Draws the text at its current position with its current opacity

class FloatingText {
Declares a class named FloatingText—this is a blueprint for creating individual floating text animations.
constructor(text, x, y, duration, speedY, fontSize, fontColor = 0) {
The constructor runs when you create a new FloatingText object with 'new FloatingText(...)' and sets up all its initial properties.
this.text = text;
Stores the text string you want to animate—'this' refers to the individual FloatingText object.
this.alpha = 255;
Sets initial opacity to 255 (fully opaque)—this will gradually decrease during the animation.
this.startTime = millis();
Records the exact millisecond when this FloatingText object was created—used later to calculate how long it's been animating.
this.active = true;
Sets a flag to true so the sketch knows this FloatingText is currently animating—when the animation finishes, active becomes false.
let elapsedTime = millis() - this.startTime;
Calculates how many milliseconds have passed since this FloatingText was created.
let progress = elapsedTime / this.duration;
Converts elapsed time into a progress value from 0 to 1—0 means just started, 1 means animation is over.
if (progress >= 1) {
Checks if the animation has finished (progress reached 1 or beyond).
this.y += this.speedY;
Moves the text upward by adding speedY to its y position—speedY is negative, so y decreases (moves up the screen).
this.alpha = map(progress, 0, 1, 255, 0);
Uses map() to convert progress (0 to 1) into an alpha value (255 to 0)—as the text animates, it fades from visible to invisible.
this.alpha = constrain(this.alpha, 0, 255);
Ensures alpha never goes below 0 or above 255, even if progress overshoots—this is a safety check.
fill(this.fontColor, this.alpha);
Sets the fill color to fontColor with the current alpha value—the second argument to fill() is the opacity (transparency).
text(this.text, this.x, this.y);
Draws the text at its current position—the position and opacity change every frame, creating the floating fade-out effect.

createFloatingText()

This helper function encapsulates all the logic needed to create and launch a floating text animation. Rather than writing all these lines in handleButtonClick() and handleUnlockButtonClick(), we wrote it once and reuse it—this follows the DRY principle (Don't Repeat Yourself) and makes the code cleaner and easier to modify.

function createFloatingText(button, message) {
  let startX = button.position().x + button.width / 2; // Center floating text horizontally above the button
  let startY = button.position().y - 20; // Start floating text 20 pixels above the button
  let duration = 2000;   // Float for 2 seconds
  let speedY = -0.5;     // Move up 0.5 pixels per frame
  let fontSize = virusInstructionSize * 1.2; // Slightly larger font size for emphasis
  let fontColor = 0;     // Black text
  floatingTexts.push(new FloatingText(message, startX, startY, duration, speedY, fontSize, fontColor));
}
Line-by-line explanation (5 lines)
let startX = button.position().x + button.width / 2;
Gets the button's left edge position, adds half its width, so the text starts centered horizontally above the button.
let startY = button.position().y - 20;
Gets the button's top edge and subtracts 20 pixels so the floating text starts 20 pixels above the button.
let duration = 2000;
The animation will last 2000 milliseconds (2 seconds) before the FloatingText becomes inactive and is removed.
let speedY = -0.5;
The text moves up 0.5 pixels per frame—negative because moving up means decreasing the y coordinate.
floatingTexts.push(new FloatingText(message, startX, startY, duration, speedY, fontSize, fontColor));
Creates a new FloatingText object with all the parameters and adds it to the floatingTexts array using push()—draw() will then animate it every frame.

handleButtonClick()

This function is called whenever someone clicks the 'Send Money' button. It's intentionally simple—its only job is to create one floating text animation. The actual details of how floating text works are hidden in the createFloatingText() function.

function handleButtonClick() {
  createFloatingText(virusButton, 'noob your computers fried forever');
}
Line-by-line explanation (1 lines)
createFloatingText(virusButton, 'noob your computers fried forever');
Calls the createFloatingText() helper function, passing the virusButton and a custom insult message—a new FloatingText object is created and added to the array.

handleUnlockButtonClick()

This is the click handler for the second button. By having separate handlers for each button, you can create different responses and make the interaction feel more personalized and threatening.

function handleUnlockButtonClick() {
  createFloatingText(unlockButton, 'no');
}
Line-by-line explanation (1 lines)
createFloatingText(unlockButton, 'no');
Calls createFloatingText() with the unlockButton and a dismissive 'no' message—clicking this button floats a different message than the first button.

windowResized()

windowResized() is a p5.js function that runs automatically whenever the browser window is resized. Without it, the canvas stays the same size and elements don't reposition. By calling this function, you make your sketch responsive—it adapts to any screen size gracefully.

function windowResized() {
  // Resize the canvas to match the new window dimensions
  resizeCanvas(windowWidth, windowHeight);
  // Update text sizes to remain responsive
  updateTextSizes();
  // Re-position the buttons based on new text sizes and canvas dimensions
  positionButtons();
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the browser window's new dimensions—called automatically whenever the user resizes their browser.
updateTextSizes();
Recalculates the text sizes based on the new window width so the title and instructions stay appropriately sized for the new screen size.
positionButtons();
Recalculates button positions based on the new canvas dimensions and updated text sizes so the buttons stay centered relative to the text.

positionButtons()

This function is a complex layout calculator. It measures text widths using textWidth(), then positions the buttons based on where the text appears on screen. This is responsive positioning—when the window resizes or text changes, this function recalculates everything so buttons stay aligned perfectly under the instruction text.

function positionButtons() {
  const instructionText = 'Send 100000 dollars to unlock computer';
  const textBeforeSpace = 'Send 100000 dollars'; // Text including "dollars"
  const singleSpace = ' ';
  const paddingBelowText = 20; // Pixels below the instruction text baseline
  const gapBetweenButtons = 200; // Increased gap between the two buttons (from 120 to 200)

  // Temporarily set text size to calculate widths accurately
  // This is crucial because textWidth() depends on the current textSize()
  textSize(virusInstructionSize);

  const w_total = textWidth(instructionText);
  const w_before_space = textWidth(textBeforeSpace); // Width up to the right edge of "dollars"
  const w_single_space = textWidth(singleSpace);

  // Calculate the x-coordinate of the center of the space
  const x_space_center = (width / 2 - w_total / 2) + w_before_space + w_single_space / 2;

  // Calculate the y-coordinate for the buttons' top edge (both buttons will be on this line)
  const y_buttons_top = (height * 0.4 + virusInstructionSize / 2) + paddingBelowText;

  // Get the widths of the buttons (p5.Element.width/height are DOM element dimensions)
  const virusButtonWidth = virusButton.width;
  const unlockButtonWidth = unlockButton.width;

  // Calculate the total width needed for both buttons and the gap
  const totalButtonsWidth = virusButtonWidth + gapBetweenButtons + unlockButtonWidth;

  // Calculate the starting x-position for the leftmost button
  const leftButtonX = x_space_center - totalButtonsWidth / 2;

  // Position the "Send Money" button (left)
  virusButton.position(leftButtonX, y_buttons_top);

  // Position the "Unlock Now" button (right)
  unlockButton.position(leftButtonX + virusButtonWidth + gapBetweenButtons, y_buttons_top);
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

calculation Text width measurements const w_total = textWidth(instructionText);

Calculates the pixel widths of various text fragments so buttons can be centered under the instruction text

calculation Button position calculations const x_space_center = (width / 2 - w_total / 2) + w_before_space + w_single_space / 2;

Determines where the space character is positioned in the instruction text so buttons appear under it

const instructionText = 'Send 100000 dollars to unlock computer';
Stores the full instruction text string—this is used to calculate the total width of the text on screen.
textSize(virusInstructionSize);
Sets the text size before measuring widths—textWidth() calculates based on the current text size, so you must set it first.
const w_total = textWidth(instructionText);
Measures the pixel width of the entire instruction text—used to find the horizontal center position.
const x_space_center = (width / 2 - w_total / 2) + w_before_space + w_single_space / 2;
Calculates the x-coordinate where the space between 'dollars' and 'to' appears on screen—the buttons will be centered under this space.
const y_buttons_top = (height * 0.4 + virusInstructionSize / 2) + paddingBelowText;
Calculates the y-coordinate for the buttons based on where the text is positioned (40% down) plus extra padding below.
const totalButtonsWidth = virusButtonWidth + gapBetweenButtons + unlockButtonWidth;
Adds up the widths of both buttons plus the gap between them—used to center both buttons together.
const leftButtonX = x_space_center - totalButtonsWidth / 2;
Calculates where the first button should start horizontally so that both buttons are centered under the space.
virusButton.position(leftButtonX, y_buttons_top);
Moves the Send Money button to its calculated position on the canvas.
unlockButton.position(leftButtonX + virusButtonWidth + gapBetweenButtons, y_buttons_top);
Moves the Unlock Now button to the right of the first button with the gap between them.

updateTextSizes()

This function makes text sizes responsive—as the window gets wider, text grows; as it gets narrower, text shrinks. The constrain() function ensures sizes stay within reasonable bounds so text is always readable. Call this whenever the window resizes or initial setup happens.

function updateTextSizes() {
  // Make the main virus title text size responsive, constraining it between 20px and 64px
  virusTextSize = constrain(width * 0.04, 20, 64);
  // Make the instruction text size responsive, constraining it between 16px and 32px
  virusInstructionSize = constrain(width * 0.02, 16, 32);
}
Line-by-line explanation (2 lines)
virusTextSize = constrain(width * 0.04, 20, 64);
Sets the title text size to 4% of the canvas width, but ensures it never goes below 20px or above 64px—this makes it responsive while staying readable.
virusInstructionSize = constrain(width * 0.02, 16, 32);
Sets the instruction text size to 2% of the canvas width, constrained between 16px and 32px—smaller than the title but still prominent.

📦 Key Variables

jumpscareImage p5.Image

Stores the first jumpscare image loaded from a URL in preload()—displayed suddenly to startle the viewer.

let jumpscareImage;
jumpscareImage2 p5.Image

Stores the second jumpscare image loaded from a URL—provides variety so jumpscares feel less predictable.

let jumpscareImage2;
jumpscareTimer number

Stores the millisecond timestamp of the last jumpscare trigger—used to measure elapsed time and trigger the next jumpscare after 13 seconds.

let jumpscareTimer;
jumpscareInterval number

Constant storing 13 seconds in milliseconds (13000)—the time delay between consecutive first jumpscares.

const jumpscareInterval = 13 * 1000;
showJumpscare boolean

A flag (true/false) indicating whether the first jumpscare image should be displayed this frame.

let showJumpscare = false;
jumpscareDuration number

Constant storing 500 milliseconds—how long the first jumpscare image stays visible before disappearing.

const jumpscareDuration = 500;
jumpscareStartTime number

Stores the millisecond timestamp when the first jumpscare started—used to measure how long it's been visible.

let jumpscareStartTime;
jumpscareTimer2 number

Stores the millisecond timestamp of the last second jumpscare trigger—independent timer for the second image.

let jumpscareTimer2;
jumpscareInterval2 number

Constant storing 5 seconds in milliseconds (5000)—the time delay between consecutive second jumpscares.

const jumpscareInterval2 = 5 * 1000;
showJumpscare2 boolean

A flag indicating whether the second jumpscare image should be displayed this frame.

let showJumpscare2 = false;
jumpscareDuration2 number

Constant storing 500 milliseconds—how long the second jumpscare image stays visible.

const jumpscareDuration2 = 500;
jumpscareStartTime2 number

Stores the millisecond timestamp when the second jumpscare started.

let jumpscareStartTime2;
virusButton p5.Renderer

Stores the HTML button element 'Send Money'—used to position it, style it, and attach click handlers.

let virusButton;
unlockButton p5.Renderer

Stores the HTML button element 'Unlock Now'—triggers a different message when clicked.

let unlockButton;
virusFont p5.Font

Stores the Roboto Mono font loaded in preload()—applied to all text in the sketch for a technical appearance.

let virusFont;
virusTextSize number

Stores the current font size for the main title 'triple t virus'—responsive and recalculated when the window resizes.

let virusTextSize = 64;
virusInstructionSize number

Stores the current font size for the instruction text—responsive and recalculated when the window resizes.

let virusInstructionSize = 32;
floatingTexts array

Stores all active FloatingText objects currently animating on screen—iterated through every frame to update and display them.

let floatingTexts = [];
noiseGen p5.Noise

Stores the white noise generator from p5.sound—plays eerie static sound in the background.

let noiseGen;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - jumpscare rendering

Both jumpscare images are loaded and checked every frame, even when not being displayed. If images are large, this wastes memory.

💡 Only call image() when showJumpscare is true—the current code does this correctly, but consider preloading images at a smaller resolution to reduce memory footprint.

BUG FloatingText.update()

The alpha value calculation uses map() which returns a float, but then constrain() is applied—the constrain is redundant if map() is functioning correctly since map() output is guaranteed to be between the output range.

💡 Remove the constrain() line, or replace map() + constrain() with a simpler formula: this.alpha = 255 * (1 - progress);

STYLE positionButtons()

The function has many intermediate calculations (w_total, w_before_space, etc.) which make the logic hard to follow. Variable names could be clearer.

💡 Rename variables to be more descriptive: 'w_total' → 'fullInstructionWidth', 'w_before_space' → 'widthBeforeDollars', 'x_space_center' → 'xPositionOfSpaceCharacter'

FEATURE FloatingText class

All floating text moves straight up at constant speed—there's no visual variety or acceleration.

💡 Add easing or gravity to the movement: this.y += this.speedY * (1 - progress); would accelerate downward as the text fades, creating a more organic fall effect.

BUG setup() - userStartAudio()

userStartAudio() is called in setup(), but many browsers require user interaction (click) before audio plays. The noise may not start until the user clicks a button.

💡 Move userStartAudio() calls into handleButtonClick() and handleUnlockButtonClick() to ensure audio plays after confirmed user interaction.

STYLE draw() - jumpscare logic

The jumpscare logic is duplicated—there are two nearly identical blocks for jumpscare 1 and jumpscare 2, violating the DRY principle.

💡 Create a helper function like updateJumpscare(timerVar, showVar, imageVar, intervalVar) to avoid duplication and make changes easier.

🔄 Code Flow

Code flow showing preload, setup, draw, floatingtext, createfloatingtext, handlebuttonclick, handleunlockbuttonclick, windowresized, positionbuttons, updatetextsizes

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> noise-setup[noise-setup] draw --> jumpscare-timer-1[jumpscare-timer-1] draw --> jumpscare-timer-2[jumpscare-timer-2] draw --> floating-text-loop[floating-text-loop] draw --> positionbuttons[positionbuttons] draw --> updatetextsizes[updatetextsizes] noise-setup --> draw jumpscare-timer-1 --> jumpscare-display-1[jumpscare-display-1] jumpscare-display-1 --> draw jumpscare-timer-2 --> draw floating-text-loop --> draw click setup href "#fn-setup" click draw href "#fn-draw" click noise-setup href "#sub-noise-setup" click jumpscare-timer-1 href "#sub-jumpscare-timer-1" click jumpscare-display-1 href "#sub-jumpscare-display-1" click jumpscare-timer-2 href "#sub-jumpscare-timer-2" click floating-text-loop href "#sub-floating-text-loop" click positionbuttons href "#fn-positionbuttons" click updatetextsizes href "#fn-updatetextsizes" floating-text-loop --> constructor[constructor] constructor --> update-method[update-method] constructor --> display-method[display-method] update-method --> draw display-method --> draw click constructor href "#sub-constructor" click update-method href "#sub-update-method" click display-method href "#sub-display-method" jumpscare-timer-1 -->|time check| jumpscare-display-1 jumpscare-timer-2 -->|time check| draw button-styling-function --> setup click button-styling-function href "#sub-button-styling-function" width-calculations --> position-calculation[position-calculation] position-calculation --> positionbuttons click width-calculations href "#sub-width-calculations" click position-calculation href "#sub-position-calculation"

❓ Frequently Asked Questions

What visual effects can users expect from the triple t virus sketch?

The sketch creates a glitchy virus warning screen featuring floating ransom text, eerie static noise, and sudden jumpscare images that flash unpredictably.

How can users interact with the triple t virus creative coding sketch?

Users can interact with ominous buttons labeled 'Send Money' and 'Unlock Now', which animate and fade as they are engaged.

What creative coding techniques are showcased in the triple t virus sketch?

The sketch demonstrates techniques such as text animation, sound generation, and the use of timers for displaying jumpscare images.

Preview

triple t virus - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of triple t virus - Code flow showing preload, setup, draw, floatingtext, createfloatingtext, handlebuttonclick, handleunlockbuttonclick, windowresized, positionbuttons, updatetextsizes
Code Flow Diagram