really bad game by corbun

This sketch creates a rippling, liquid-metal landscape rendered with GLSL shaders that responds to mouse movement in real time. As you move your cursor faster, the visual distortions intensify and trigger evolving techno synth sounds with glitchy effects, creating a fully interactive audio-visual experience.

🧪 Try This!

Experiment with the code by making these changes:

  1. Shift synth pitch mapping — Change the peak frequency the synth reaches—try mapping velocity to 100–800 Hz instead of 50–400 to make the effect brighter and more aggressive.
  2. Increase ripple persistence — Reduce velocity damping so ripples linger longer on the screen before fading—the visual effect and audio tail become more continuous and dreamy.
  3. Enable more glitchy distortion — Raise the maximum distortion so the synth sounds more broken and digital at high velocities—the audio becomes more alien and experimental.
  4. Change synth waveform — Swap the sawtooth waveform for a sine wave to get a pure, smooth tone instead of a buzzy bright sound—the techno feel becomes more ethereal.
  5. Boost reverb decay — Increase reverb decay time from 3 seconds to 6 seconds to make the synth sound like it's echoing in a massive cathedral instead of a tight room.
  6. Lower glitch threshold — Make the synth glitch at lower velocities—change the 30 threshold to 15 so you hear pitch jumps even with modest mouse movement.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch generates a mesmerizing liquid-chrome visual landscape that flows and distorts in response to your mouse movement. The effect is powered by GLSL fragment shaders (compiled GPU code), p5.sound's oscillators and effects processors, and real-time mouse velocity calculations that feed into both the shader uniforms and the audio synthesis parameters. As you move your mouse faster, the ripples intensify, the synth pitch rises and glitches, and distortion climbs—creating a tightly choreographed audio-visual feedback loop.

The code is organized into a preload() function that loads the shader files and font, a setup() function that initializes the audio processing chain and WEBGL canvas, a draw() function that calculates mouse velocity and passes it to both audio and visuals, and a windowResized() function that keeps the shader resolution in sync. By studying it, you will learn how to architect a real-time shader-driven sketch, route audio effects in series, modulate sound parameters with interaction data, and handle GPU-accelerated rendering in p5.js.

⚙️ How It Works

  1. When the sketch loads, preload() fetches the vertex and fragment shader files from disk, loads a web font, and initializes mouse tracking variables to zero.
  2. setup() creates a full-screen WEBGL canvas (GPU-accelerated drawing mode), initializes a sawtooth oscillator synth, and chains three audio effects (reverb, delay, distortion) in series so sound flows through all of them.
  3. Every frame, draw() calculates how fast and in which direction the mouse moved by subtracting its previous position from its current position, scaled smoothly with damping (multiply by 0.95 each frame so velocity naturally decays).
  4. The calculated velocity magnitude is mapped to the synth's frequency (faster movement = higher pitch) and amplitude (louder), and also controls the distortion amount and delay feedback, creating glitchy effects at high velocities.
  5. The shader is activated with shader(), and uniforms (time, mouse position, velocity magnitude, canvas resolution) are passed to the GPU, which then renders the liquid-chrome effect on a full-screen rectangle.
  6. Optionally, text 'made by corbun' is drawn in the bottom-left corner using WEBGL text positioning.

🎓 Concepts You'll Learn

GLSL shaders and GPU renderingShader uniforms and real-time data passingMouse velocity calculationp5.sound oscillators and audio effectsEffect chaining and audio routingWEBGL mode in p5.jsSmooth animation with dampingAudio-visual synchronization

📝 Code Breakdown

preload()

preload() runs before setup() and before the first draw() frame. It is the place to load files (shaders, fonts, images) that must be ready before your sketch begins. The shader files you load here are the GPU programs that create the liquid-chrome visual effect—they run on every pixel of the canvas in parallel.

function preload() {
  // Load the custom shaders
  // Ensure 'vertex.glsl' and 'fragment.glsl' files are present and contain GLSL code
  myShader = loadShader('vertex.glsl', 'fragment.glsl');
  
  // Load a font for text in WEBGL mode
  // Using Fontsource CDN for reliable WEBGL-compatible fonts
  myFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');

  // Initialize velocity variables for the first frame
  prevMouseX = mouseX;
  prevMouseY = mouseY;
  velocityX = 0;
  velocityY = 0;
  velocityMagnitude = 0;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Shader Loading myShader = loadShader('vertex.glsl', 'fragment.glsl');

Loads the vertex and fragment shader files from disk and compiles them for GPU execution

function-call Font Loading myFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');

Fetches a WEBGL-compatible font from a CDN so text renders crisp in GPU mode

calculation Velocity Initialization prevMouseX = mouseX; prevMouseY = mouseY; velocityX = 0; velocityY = 0; velocityMagnitude = 0;

Sets up mouse tracking variables so the first frame's velocity calculation has a valid starting point

myShader = loadShader('vertex.glsl', 'fragment.glsl');
Loads two GLSL files from disk (vertex.glsl and fragment.glsl), compiles them, and stores the compiled shader program in myShader so it can be used later in draw()
myFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
Fetches a web-safe font from a CDN that works reliably in WEBGL mode, storing it in myFont for later text rendering
prevMouseX = mouseX;
Stores the current mouse X position so that in the first frame of draw(), we can calculate how far the mouse has moved since preload ran
velocityX = 0;
Sets horizontal velocity to zero before draw() starts, so the sketch doesn't interpret the first mouse movement as infinite velocity

setup()

setup() runs once after preload() and before the first draw() frame. It is where you initialize your canvas, load settings, and start audio playback. The audio effects are chained in series: synth → reverb → delay → distortion, so audio flows through all of them. Each effect has an amp() setting that controls how loud that effect is in the mix.

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  noStroke(); // No p5.js strokes, shader handles all rendering

  // Set the loaded font and text size for the "made by corbun" text
  textFont(myFont);
  textSize(16); // Increased text size
  fill(255); // White text color

  // Initialize p5.sound components for techno music
  // Using a sawtooth wave for a typical techno synth sound
  technoSynth = new p5.Oscillator('sawtooth');
  technoSynth.amp(0); // Start silent
  technoSynth.freq(100); // Base frequency
  technoSynth.start();

  // Add reverb for a spacious, metallic sound
  technoReverb = new p5.Reverb();
  technoReverb.process(technoSynth, 3, 2); // 3 seconds decay, 2% dry/wet
  technoReverb.amp(0.1); // Subtle reverb volume

  // Add delay for a rhythmic echo effect
  technoDelay = new p5.Delay();
  technoDelay.process(technoSynth, 0.25, 0.5, 2300); // 0.25 delay time, 0.5 feedback, 2300 ms filter frequency
  technoDelay.amp(0.1); // Subtle delay volume

  // Add a distortion effect for the glitchy sound
  technoDistortion = new p5.Distortion(0.0); // Start with no distortion
  technoDistortion.process(technoSynth);
  technoDistortion.amp(0.1); // Subtle distortion volume

  // Start the Web Audio context. This is required for audio to play in modern browsers.
  userStartAudio();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

function-call Canvas Creation createCanvas(windowWidth, windowHeight, WEBGL);

Creates a full-screen canvas in WEBGL mode, which enables GPU-accelerated rendering for shaders

calculation Synth Oscillator Setup technoSynth = new p5.Oscillator('sawtooth'); technoSynth.amp(0); technoSynth.freq(100); technoSynth.start();

Creates a sawtooth wave oscillator, sets it to silent (amp 0), and starts playback so it is ready to modulate when draw() runs

calculation Audio Effects Chain technoReverb = new p5.Reverb(); technoReverb.process(technoSynth, 3, 2); technoDelay = new p5.Delay(); technoDelay.process(technoSynth, 0.25, 0.5, 2300); technoDistortion = new p5.Distortion(0.0); technoDistortion.process(technoSynth);

Creates three audio effects and chains them to the oscillator so the synth's output flows through reverb, delay, and distortion in sequence

createCanvas(windowWidth, windowHeight, WEBGL);
Creates a canvas the size of the window in WEBGL mode—GPU rendering that lets shaders run on every pixel in parallel
noStroke();
Disables p5.js stroke drawing so shapes are filled only—the shader is handling all the visual complexity
textFont(myFont);
Tells p5.js to use the loaded roboto font for any text() calls that follow
technoSynth = new p5.Oscillator('sawtooth');
Creates a new oscillator using a sawtooth waveform, which is a bright, buzzsaw-like sound typical in techno music
technoSynth.amp(0);
Sets the oscillator's amplitude to 0 so it is silent on startup—draw() will modulate this based on mouse velocity
technoSynth.start();
Starts the oscillator playing (silently, since amp is 0), so it is ready to make sound when draw() increases its amplitude
technoReverb = new p5.Reverb();
Creates a reverb effect that will add space and echo to the synth
technoReverb.process(technoSynth, 3, 2);
Connects the reverb to the synth, with a 3-second decay time and 2% wet/dry mix (mostly dry, subtle reverb)
technoDelay.process(technoSynth, 0.25, 0.5, 2300);
Connects the delay to the synth with a 0.25-second delay time, 0.5 feedback (echoes repeat at 50% volume), and a 2300 Hz filter
technoDistortion.process(technoSynth);
Connects the distortion effect to the synth so it will add grit and glitchiness as velocityMagnitude increases
userStartAudio();
Starts the Web Audio context—required in modern browsers to enable audio playback on user interaction (instead of auto-play)

draw()

draw() is the main animation loop, running 60 times per second. Here you calculate the mouse velocity (a core real-time input), modulate the audio parameters (oscillator frequency, distortion, delay feedback) based on that velocity, activate the shader, pass uniforms (variables) to the GPU, and render the full-screen rectangle. The shader then takes those uniforms and generates the liquid-chrome visual effect on the GPU, making it extremely fast and responsive.

function draw() {
  // Calculate mouse velocity for this frame
  // Multiply by 0.5 to make the effect less aggressive and allow for smoother transitions
  velocityX = (mouseX - prevMouseX) * 0.5;
  velocityY = (mouseY - prevMouseY) * 0.5;

  // Store current mouse position for the next frame's velocity calculation
  prevMouseX = mouseX;
  prevMouseY = mouseY;

  // Smoothly decay velocity over time, making ripples settle
  // This gives the "liquid mercury" feel where movement creates ripples that fade
  velocityX *= 0.95;
  velocityY *= 0.95;

  // Calculate the magnitude (length) of the velocity vector
  velocityMagnitude = sqrt(velocityX * velocityX + velocityY * velocityY);

  // Modulate techno music based on velocityMagnitude
  // Map velocityMagnitude to frequency and amplitude of the synth
  // This makes the music more intense and glitchy as you move the mouse faster

  // Base frequency mapping
  let baseFreq = map(velocityMagnitude, 0, 100, 50, 400); // Map velocity (0-100) to frequency (50-400 Hz)
  let freq = baseFreq;

  // Add random frequency jumps for glitchiness when velocity is high
  if (velocityMagnitude > 30) {
    freq += random(-20, 20); // Add a small random offset
  }
  if (velocityMagnitude > 60) {
    freq += random(-50, 50); // Add a larger random offset
  }
  freq = constrain(freq, 50, 600); // Keep frequency within a reasonable range

  let amp = map(velocityMagnitude, 0, 100, 0, 0.2); // Map velocity (0-100) to amplitude (0-0.2 volume)

  technoSynth.freq(freq);
  technoSynth.amp(amp, 0.1); // Smooth amplitude change over 0.1 seconds

  // Modulate distortion amount based on velocityMagnitude
  // Faster movement = more distortion
  let distortionAmount = map(velocityMagnitude, 0, 100, 0, 0.5); // Map velocity to distortion amount (0-0.5)
  technoDistortion.amount.value = distortionAmount;

  // Randomize delay feedback slightly for glitchiness when velocity is high
  if (velocityMagnitude > 40) {
    let delayFeedback = map(velocityMagnitude, 0, 100, 0.5, 0.7); // Base feedback
    delayFeedback += random(-0.05, 0.05); // Add random variation
    delayFeedback = constrain(delayFeedback, 0.4, 0.8); // Keep feedback within a reasonable range
    technoDelay.feedback.value = delayFeedback;
  } else {
    technoDelay.feedback.value = 0.5; // Default feedback when calm
  }

  // Set the custom shader to be used for rendering
  shader(myShader);

  // Pass uniforms (variables) to the fragment shader
  myShader.setUniform('u_resolution', [width, height]); // Canvas resolution
  myShader.setUniform('u_mouse', [mouseX, mouseY]);     // Mouse position
  myShader.setUniform('u_velocityMagnitude', velocityMagnitude); // Mouse velocity magnitude
  myShader.setUniform('u_time', millis() / 1000.0);      // Time in seconds

  // Draw a rectangle that covers the entire canvas.
  // The shader will then render the liquid chrome effect on this rectangle.
  rect(0, 0, width, height);

  // Draw "made by corbun" text
  // In WEBGL, (0,0) is the center. To place in bottom-left, use negative width/2 and positive height/2.
  text("made by corbun", -width/2 + 10, height/2 - 10);
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Mouse Velocity Calculation velocityX = (mouseX - prevMouseX) * 0.5; velocityY = (mouseY - prevMouseY) * 0.5;

Calculates how fast the mouse moved this frame by subtracting last frame's position from this frame's, then scales by 0.5 for smoothing

calculation Velocity Damping velocityX *= 0.95; velocityY *= 0.95;

Decays velocity every frame so ripples and audio effects naturally settle when the mouse stops moving

calculation Velocity Magnitude velocityMagnitude = sqrt(velocityX * velocityX + velocityY * velocityY);

Computes the total speed (distance) of the velocity vector, used to modulate both visuals and audio

conditional Glitchy Frequency Jumps if (velocityMagnitude > 30) { freq += random(-20, 20); } if (velocityMagnitude > 60) { freq += random(-50, 50); }

Adds random pitch shifts when moving fast, creating the glitchy, unpredictable synth sound

calculation Distortion Modulation let distortionAmount = map(velocityMagnitude, 0, 100, 0, 0.5); technoDistortion.amount.value = distortionAmount;

Maps velocity to distortion intensity so faster movement = grittier sound

calculation Shader Rendering shader(myShader); myShader.setUniform('u_resolution', [width, height]); myShader.setUniform('u_mouse', [mouseX, mouseY]); myShader.setUniform('u_velocityMagnitude', velocityMagnitude); myShader.setUniform('u_time', millis() / 1000.0); rect(0, 0, width, height);

Activates the shader, passes data (resolution, mouse, velocity, time) to the GPU, and renders a full-screen rectangle using the shader program

velocityX = (mouseX - prevMouseX) * 0.5;
Calculates horizontal velocity by finding the difference between current and previous X position, scaled by 0.5 to smooth the effect
prevMouseX = mouseX;
Stores the current mouse X for next frame, so the next velocity calculation has the right 'previous' value
velocityX *= 0.95;
Multiplies velocity by 0.95, so it loses 5% every frame—ripples and audio effects slowly die away when you stop moving
velocityMagnitude = sqrt(velocityX * velocityX + velocityY * velocityY);
Computes the Euclidean length of the velocity vector: the total speed regardless of direction
let baseFreq = map(velocityMagnitude, 0, 100, 50, 400);
Maps velocity (0–100 range) to synth frequency (50–400 Hz range), so faster movement = higher pitch
if (velocityMagnitude > 30) { freq += random(-20, 20); }
When velocity exceeds 30, adds a random pitch shift of ±20 Hz to the synth, making it sound glitchy and unpredictable
technoSynth.freq(freq);
Sets the synth's frequency to the calculated (and possibly randomized) value
technoSynth.amp(amp, 0.1);
Sets the synth's amplitude to the mapped value, with 0.1-second smooth ramp so volume changes feel natural, not instant
let distortionAmount = map(velocityMagnitude, 0, 100, 0, 0.5);
Maps velocity to distortion strength (0–0.5 range), so faster movement adds more audio grit
technoDistortion.amount.value = distortionAmount;
Applies the distortion amount to the effect, creating the glitchy, broken sound at high velocities
shader(myShader);
Activates the loaded shader program so all subsequent drawing uses GPU code instead of p5.js defaults
myShader.setUniform('u_resolution', [width, height]);
Passes the canvas width and height to the shader so it can calculate pixel coordinates and effects correctly
myShader.setUniform('u_mouse', [mouseX, mouseY]);
Sends the current mouse position to the shader, allowing it to center ripples or distortions around your cursor
myShader.setUniform('u_velocityMagnitude', velocityMagnitude);
Passes mouse velocity to the shader so it can intensify ripples and distortions based on how fast you move
myShader.setUniform('u_time', millis() / 1000.0);
Passes elapsed time in seconds to the shader, enabling smooth time-based animation like waves or pulsing effects
rect(0, 0, width, height);
Draws a full-screen rectangle—the shader runs on every pixel of this rectangle, generating the liquid-chrome effect
text("made by corbun", -width/2 + 10, height/2 - 10);
Draws the text 'made by corbun' in the bottom-left corner; in WEBGL mode, (0,0) is the center, so negative width/2 is the left edge

windowResized()

windowResized() is a p5.js callback that runs automatically whenever the browser window is resized. By updating the canvas size and the shader's resolution uniform, you ensure the liquid-chrome effect stays sharp and responsive no matter how large or small the window becomes.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Update the resolution uniform in the shader when the canvas is resized
  myShader.setUniform('u_resolution', [width, height]);
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

function-call Canvas Resize resizeCanvas(windowWidth, windowHeight);

Resizes the p5.js canvas to match the new window dimensions

function-call Shader Resolution Update myShader.setUniform('u_resolution', [width, height]);

Passes the new resolution to the shader so it can recalculate pixel coordinates and effects correctly

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to the new window dimensions when the browser window is resized
myShader.setUniform('u_resolution', [width, height]);
Updates the shader's resolution uniform so the liquid-chrome effect scales correctly to the new canvas size

📦 Key Variables

myShader p5.Shader

Stores the compiled GLSL shader program loaded from vertex.glsl and fragment.glsl; used to render the liquid-chrome effect on the GPU

let myShader;
prevMouseX number

Stores the mouse's X position from the previous frame; used to calculate how far the mouse moved horizontally this frame

let prevMouseX = 0;
prevMouseY number

Stores the mouse's Y position from the previous frame; used to calculate how far the mouse moved vertically this frame

let prevMouseY = 0;
velocityX number

Horizontal component of mouse velocity; calculated as the change in X position, damped each frame, and used to modulate audio and pass to the shader

let velocityX = 0;
velocityY number

Vertical component of mouse velocity; calculated as the change in Y position, damped each frame, and used to modulate audio and pass to the shader

let velocityY = 0;
velocityMagnitude number

Total speed of the mouse (Euclidean length of the velocity vector); controls synth pitch/volume, distortion, delay feedback, and shader ripple intensity

let velocityMagnitude = 0;
technoSynth p5.Oscillator

A sawtooth oscillator that generates the base synth sound; its frequency and amplitude are modulated by mouse velocity in draw()

let technoSynth;
technoReverb p5.Reverb

Audio reverb effect chained to the synth, adding space and metallic echo to the sound

let technoReverb;
technoDelay p5.Delay

Audio delay effect chained to the synth, adding rhythmic echoes; its feedback amount is modulated by velocity for glitchiness

let technoDelay;
technoDistortion p5.Distortion

Audio distortion effect chained to the synth; its intensity increases with mouse velocity to create glitchy, broken sounds at high speeds

let technoDistortion;
myFont p5.Font

Stores the loaded Roboto font from the Fontsource CDN; used for rendering the 'made by corbun' text in WEBGL mode

let myFont;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG draw() shader uniforms

If windowResized() is called before shader uniforms are first set, the shader may render with stale or uninitialized resolution values, causing visual glitches

💡 Move the myShader.setUniform('u_resolution', ...) call into setup() after shader(myShader) so it is always initialized, then keep the update in windowResized()

PERFORMANCE draw() frequency calculation

random() is called multiple times per frame inside conditionals, which is expensive and can cause audio clicks if called inconsistently across frames

💡 Cache random values at the start of each frame or use Perlin noise (noise()) for smoother, less jarring glitches

STYLE preload() and draw()

Velocity variables are updated in draw() but initialized in preload()—this splits initialization across two functions and could confuse readers

💡 Initialize velocityX, velocityY, velocityMagnitude as global let declarations at the top of the file so all state is in one place

FEATURE setup() audio

All audio effects are hardcoded to fixed mix levels (0.1)—there is no way to adjust the reverb, delay, or distortion amounts without editing code

💡 Add interactive controls (sliders or keyboard shortcuts) to adjust effect volumes and reverb decay time in real time

BUG draw() delay feedback modulation

Delay feedback is only updated when velocityMagnitude > 40; if velocity drops below 40, feedback snaps to 0.5 immediately, which could cause audio artifacts

💡 Smooth the delay feedback transition using lerp() so it glides between states instead of snapping abruptly

🔄 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] --> preload[preload] preload --> setup[setup] click preload href "#fn-preload" click setup href "#fn-setup" setup --> canvas-setup[canvas-setup] canvas-setup --> font-load[font-load] click canvas-setup href "#sub-canvas-setup" click font-load href "#sub-font-load" setup --> velocity-init[velocity-init] click velocity-init href "#sub-velocity-init" setup --> oscillator-init[oscillator-init] click oscillator-init href "#sub-oscillator-init" setup --> effects-chain[effects-chain] click effects-chain href "#sub-effects-chain" setup --> draw[draw loop] click draw href "#fn-draw" draw --> velocity-calc[velocity-calc] click velocity-calc href "#sub-velocity-calc" draw --> velocity-damping[velocity-damping] click velocity-damping href "#sub-velocity-damping" draw --> magnitude-calc[magnitude-calc] click magnitude-calc href "#sub-magnitude-calc" draw --> freq-glitch[freq-glitch] click freq-glitch href "#sub-freq-glitch" draw --> distortion-modulate[distortion-modulate] click distortion-modulate href "#sub-distortion-modulate" draw --> shader-render[shader-render] click shader-render href "#sub-shader-render" shader-render --> windowresized[windowresized] click windowresized href "#fn-windowresized" windowresized --> canvas-resize[canvas-resize] click canvas-resize href "#sub-canvas-resize" canvas-resize --> uniform-update[uniform-update] click uniform-update href "#sub-uniform-update" windowresized --> draw

❓ Frequently Asked Questions

What kind of visual experience does the 'really bad game by corbun' sketch offer?

This sketch transforms the screen into a rippling, liquid-metal landscape that smoothly reacts to mouse movement, creating flowing waves and shimmering distortions.

How can users interact with the 'really bad game by corbun' sketch?

Users can interact by moving their mouse, which influences the visual effects and syncs with evolving techno synth sounds.

What creative coding concepts are showcased in this p5.js sketch?

The sketch demonstrates advanced shader techniques for rendering dynamic visuals and integrates sound manipulation using the p5.sound library for an immersive audio-visual experience.

Preview

really bad game by corbun - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of really bad game by corbun - Code flow showing preload, setup, draw, windowresized
Code Flow Diagram