SystemTest2

This sketch splits the screen into four live panels that each demonstrate a different hardware capability p5.js can access: mouse/touch input with ripple effects, device motion and compass sensors, live webcam video, and a microphone-reactive 3D cube rendered with WebGL. It works as an interactive test harness where every quadrant reacts instantly to a different piece of hardware.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the ripples — Increasing the growth rate makes click/tap ripples expand much faster across the pointer panel.
  2. Make ripples linger longer — Lowering the fade rate makes each ripple stay visible much longer before disappearing.
  3. Enlarge touch indicators — The last argument of circle() is the touch indicator's diameter - make it huge to see it clearly on a touchscreen.
  4. Speed up the cube's idle spin — When no keys are held and no sensors are active, the cube spins slowly on its own - this makes that resting spin much faster.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch divides the canvas into four quadrants, each showcasing a different device capability p5.js can tap into: touch and mouse ripples, device orientation and acceleration, live webcam video, and a microphone-reactive 3D cube rendered with WebGL. It mixes ordinary 2D drawing calls like rect(), circle(), and text() with an off-screen WEBGL graphics buffer created via createGraphics(), plus p5.sound's AudioIn and Amplitude classes for real-time audio analysis. Handling permissions correctly for camera, microphone, and iOS motion sensors is a core theme, since every one of these features only works after the user grants access through a browser gesture.

The code is organized as one small panel-drawing function per quadrant, all called from a single draw() loop, with shared helper functions handling ripple animation, cube rotation, and permission requests. By studying it you'll learn how to combine several input APIs at once - mouseMoved, touchStarted, keyPressed - with sensor globals like accelerationX and rotationZ, and how to safely gate hardware features behind explicit user gestures.

⚙️ How It Works

  1. On load, setup() builds a full-window canvas, starts a webcam capture, creates a microphone input with an amplitude analyzer, and builds a separate 260x260 WEBGL graphics buffer just for the 3D cube.
  2. Every frame, draw() clears the background and calls four panel functions in sequence, one per quadrant, then draws divider lines and a header on top.
  3. The pointer panel tracks mouse and touch positions, tints its background based on lastInputType, and spawns expanding ripple circles whenever the user clicks or taps inside it.
  4. The motion panel reads p5's automatically-updated accelerationX/Y/Z and rotationX/Y/Z globals and turns them into moving gauge bars and a compass needle.
  5. The audio/3D panel reads the microphone's amplitude level, feeds it into updateCubeRotation() to spin and recolor a WebGL cube, and draws a noise-flavored bar-graph audio meter beside it.
  6. Clicking or tapping the canvas fires ensureAudioStarted() and ensureSensorsActivated(), which request the browser permissions needed for microphone and motion access, while keyPressed() lets arrow keys spin the cube, H trigger a vibration, and M mute the mic.

🎓 Concepts You'll Learn

createCapture (webcam video)p5.sound AudioIn & Amplitude analysisOff-screen WEBGL graphics with createGraphicsDevice motion/orientation sensorsTouch & mouse event handlingnavigator.vibrate haptic feedbackPermission-gated browser APIsParticle-style ripple animation with arrays

📝 Code Breakdown

setup()

setup() runs once and is the right place to request hardware access (webcam, microphone) and build any off-screen buffers you'll reuse every frame, like the WEBGL graphics object here.

function setup() {
  createCanvas(windowWidth, windowHeight); // 2D canvas
  angleMode(RADIANS);                      // For 3D rotations

  // Webcam
  cam = createCapture(VIDEO); // https://p5js.org/reference/#/p5/createCapture
  cam.size(320, 240);
  cam.hide();                 // We'll draw it on the canvas manually

  // Microphone + amplitude analyzer
  mic = new p5.AudioIn();     // https://p5js.org/reference/#/p5.AudioIn
  amp = new p5.Amplitude();   // https://p5js.org/reference/#/p5.Amplitude
  amp.setInput(mic);

  // Off-screen WEBGL buffer for 3D cube (uses WebGL2 where available)
  g3d = createGraphics(260, 260, WEBGL);

  textFont('sans-serif');
}
Line-by-line explanation (9 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the whole browser window, so the four quadrant panels can be sized from width/height.
angleMode(RADIANS);
Tells p5 that rotation values (like rotateX) are given in radians instead of degrees - important since rotationX/Y/Z sensor readings get converted with radians().
cam = createCapture(VIDEO);
Requests webcam access from the browser and returns a video element that updates automatically.
cam.size(320, 240);
Requests the webcam stream at 320x240 pixels, keeping the video light-weight to draw.
cam.hide();
Hides the raw HTML <video> element so only the version drawn manually with image() inside the canvas is visible.
mic = new p5.AudioIn();
Creates a microphone input object from the p5.sound library, ready to be started later on a user gesture.
amp = new p5.Amplitude();
Creates an analyzer that can report the overall loudness (0-1) of whatever audio source it's connected to.
amp.setInput(mic);
Connects the amplitude analyzer to the microphone so amp.getLevel() reports the mic's volume.
g3d = createGraphics(260, 260, WEBGL);
Creates a separate off-screen 3D drawing surface. Rendering the cube here keeps 3D code isolated from the main 2D canvas.

draw()

draw() runs continuously (about 60 times per second) and is the conductor of the sketch - it doesn't draw much itself, but calls out to each panel function in a fixed order every frame.

🔬 This draws the cross-shaped divider between panels. What happens if you change strokeWeight(1) to strokeWeight(10), or change stroke(80) to a bright color like stroke(255, 0, 0)?

  stroke(80);
  strokeWeight(1);
  line(halfW, 0, halfW, height);
  line(0, halfH, width, halfH);
function draw() {
  background(10);

  const halfW = width / 2;
  const halfH = height / 2;

  // Panels
  drawPointerPanel(0, 0, halfW, halfH);
  drawMotionPanel(halfW, 0, halfW, halfH);
  drawCameraPanel(0, halfH, halfW, halfH);
  drawAudioAnd3DPanel(halfW, halfH, halfW, halfH);

  // Panel dividers
  stroke(80);
  strokeWeight(1);
  line(halfW, 0, halfW, height);
  line(0, halfH, width, halfH);

  // Header / instructions
  drawHeader();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Four Panel Calls drawPointerPanel(0, 0, halfW, halfH);

Draws each quadrant by handing it its own x, y, width, and height so panel code can be self-contained.

background(10);
Clears the whole canvas to near-black at the start of every frame, before any panel is drawn on top.
const halfW = width / 2;
Calculates half the canvas width once per frame so every panel and the divider lines can share it.
drawPointerPanel(0, 0, halfW, halfH);
Draws the top-left panel, passing its origin (0,0) and size (halfW x halfH) as arguments.
line(halfW, 0, halfW, height);
Draws the vertical line that visually separates the left and right panels.
line(0, halfH, width, halfH);
Draws the horizontal line that separates the top and bottom panels.
drawHeader();
Draws the title and instructions on top of everything else, last, so it's never covered by a panel.

drawPointerPanel()

This function shows how to read both mouse position (mouseX/mouseY) and the touches array in the same panel, letting one sketch respond naturally to desktop and touchscreen users alike.

🔬 This loop only draws a circle for touches that land inside this panel. What happens if you remove the if-check so every touch draws a circle here, even when your finger is in a different panel?

  for (const t of touches) {
    if (t.x >= x && t.x <= x + w && t.y >= y && t.y <= y + h) {
      circle(t.x, t.y, 30);
    }
  }
function drawPointerPanel(x, y, w, h) {
  push();
  noStroke();

  let baseColor;
  if (lastInputType === 'mouse') {
    baseColor = color(40, 80, 140);
  } else if (lastInputType === 'touch') {
    baseColor = color(40, 120, 80);
  } else {
    baseColor = color(40, 40, 60);
  }
  fill(baseColor);
  rect(x, y, w, h);

  // Ripples from clicks/taps
  drawRipplesInPanel(x, y, w, h);

  // Mouse pointer indicator (if inside this panel)
  if (mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h) {
    fill(255);
    noStroke();
    circle(mouseX, mouseY, 16);
  }

  // Touch indicators
  fill(0, 200, 255, 130);
  noStroke();
  for (const t of touches) {
    if (t.x >= x && t.x <= x + w && t.y >= y && t.y <= y + h) {
      circle(t.x, t.y, 30);
    }
  }

  // Text info
  fill(255);
  textSize(14);
  textAlign(LEFT, TOP);
  text('POINTER & TOUCH', x + 10, y + 10);

  textSize(12);
  text('Last input: ' + lastInputType, x + 10, y + 32);
  text('Mouse: ' + int(mouseX) + ', ' + int(mouseY), x + 10, y + 50);
  text('Touch points: ' + touches.length, x + 10, y + 68);
  text('Drag / tap in this corner to create ripples.', x + 10, y + 88);

  pop();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Background Color by Input Type if (lastInputType === 'mouse') { ... } else if (lastInputType === 'touch') { ... }

Tints the whole panel a different color depending on whether the last input was mouse, touch, or nothing yet.

for-loop Touch Point Indicators for (const t of touches) { ... }

Draws a translucent cyan circle at every active touch point that falls inside this panel.

if (lastInputType === 'mouse') {
Checks which kind of input was used most recently to pick a background tint - blue for mouse.
fill(baseColor);
Applies the chosen tint before drawing the panel's background rectangle.
drawRipplesInPanel(x, y, w, h);
Delegates to a helper function that animates and draws any active click/tap ripples inside this panel's bounds.
if (mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h) {
Only draws the white mouse-pointer dot if the mouse is currently over this specific panel.
for (const t of touches) {
Loops over p5's built-in touches array, which contains one entry per active finger on a touchscreen.
text('Touch points: ' + touches.length, x + 10, y + 68);
Displays how many simultaneous touches are currently active, useful for testing multi-touch.

drawMotionPanel()

This function shows how p5.js exposes device motion sensors as simple global variables (accelerationX, rotationZ, etc.) that update automatically - no manual event listeners required - making it easy to turn raw sensor numbers into a readable gauge.

🔬 Each call's third argument (30) sets the max acceleration the bar can show before it clamps. What happens if you lower all three to 5 - do the bars saturate more easily when you move your device?

  // Acceleration rows
  drawAxisRow('accel X', ax, 30, 0);
  drawAxisRow('accel Y', ay, 30, 1);
  drawAxisRow('accel Z', az, 30, 2);
function drawMotionPanel(x, y, w, h) {
  push();
  translate(x, y);
  noStroke();
  fill(30, 40, 60);
  rect(0, 0, w, h);

  fill(255);
  textSize(14);
  textAlign(LEFT, TOP);
  text('MOTION & COMPASS', 10, 10);

  textSize(12);

  // Device acceleration / rotation (p5 auto-updates these)
  // https://p5js.org/reference/#/p5/accelerationX
  const ax = accelerationX || 0;
  const ay = accelerationY || 0;
  const az = accelerationZ || 0;
  const rx = rotationX || 0;
  const ry = rotationY || 0;
  const rz = rotationZ || 0;

  const barOriginX = w * 0.25;
  const barWidth = w * 0.5;
  let rowY = 40;

  function drawAxisRow(label, value, maxAbs, rowIdx) {
    const yRow = rowY + rowIdx * 32;
    stroke(100);
    line(barOriginX, yRow, barOriginX + barWidth, yRow);

    const len = constrain(
      map(value, -maxAbs, maxAbs, -barWidth / 2, barWidth / 2),
      -barWidth / 2,
      barWidth / 2
    );

    stroke(0, 255, 160);
    strokeWeight(4);
    line(barOriginX + barWidth / 2, yRow, barOriginX + barWidth / 2 + len, yRow);

    noStroke();
    fill(230);
    text(
      label + ': ' + nf(value, 1, 1),
      10,
      yRow - 8
    );
  }

  // Acceleration rows
  drawAxisRow('accel X', ax, 30, 0);
  drawAxisRow('accel Y', ay, 30, 1);
  drawAxisRow('accel Z', az, 30, 2);

  // Rotation rows
  rowY += 110;
  drawAxisRow('rot X (pitch)', rx, 180, 0);
  drawAxisRow('rot Y (roll)', ry, 180, 1);
  drawAxisRow('rot Z (yaw)', rz, 180, 2);

  // Compass-like indicator using rotationZ
  const headingRad = radians(rz);
  const cx = w * 0.7;
  const cy = h * 0.7;
  const r = min(w, h) * 0.18;

  push();
  translate(cx, cy);
  noFill();
  stroke(200);
  strokeWeight(1.5);
  ellipse(0, 0, r * 2, r * 2);

  // Arrow pointing in approximate "heading"
  stroke(0, 255, 160);
  strokeWeight(3);
  line(0, 0, r * sin(headingRad), -r * cos(headingRad));

  // North label
  noStroke();
  fill(220);
  textAlign(CENTER, BOTTOM);
  text('N', 0, -r - 4);
  pop();

  // Status text
  fill(230);
  text(
    'rotationZ ≈ heading: ' + nf(rz, 1, 1) + '°',
    10,
    h - 60
  );
  text(
    'Sensors active: ' + (motionPermissionAsked ? (motionPermissionGranted ? 'yes' : 'denied') : 'auto'),
    10,
    h - 44
  );
  text(
    'Tilt / rotate your device to see the bars + compass move.',
    10,
    h - 28
  );

  pop();
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Acceleration Bar Rows drawAxisRow('accel X', ax, 30, 0);

Draws a horizontal gauge bar showing the current X/Y/Z acceleration reading.

calculation Rotation Bar Rows drawAxisRow('rot X (pitch)', rx, 180, 0);

Draws gauge bars for the device's pitch, roll, and yaw rotation in degrees.

calculation Compass Needle line(0, 0, r * sin(headingRad), -r * cos(headingRad));

Draws a needle inside a circle that points in the direction implied by rotationZ, mimicking a compass.

translate(x, y);
Shifts the drawing origin to this panel's top-left corner, so all following coordinates can be written as if (0,0) were local to this panel.
const ax = accelerationX || 0;
Reads p5's built-in accelerationX global (auto-updated by the browser's device motion API) and falls back to 0 if it's undefined.
const rz = rotationZ || 0;
Reads the device's yaw rotation in degrees, used later to drive the compass needle.
function drawAxisRow(label, value, maxAbs, rowIdx) {
Defines a small helper function right inside drawMotionPanel so it can reuse local variables like rowY and barWidth without passing them every time.
drawAxisRow('accel X', ax, 30, 0);
Calls the helper to draw one bar for the X acceleration reading, using 30 as the expected maximum magnitude.
const headingRad = radians(rz);
Converts the rotationZ value from degrees to radians so it can be used with sin()/cos() to position the compass needle.
line(0, 0, r * sin(headingRad), -r * cos(headingRad));
Draws a line from the compass center outward in the direction of headingRad, creating the needle.

drawAxisRow() (nested helper inside drawMotionPanel)

This is a classic map() + constrain() pattern for building gauges: map() rescales a value from one range to another, and constrain() prevents it from breaking the visual bounds when the input goes beyond what you expected.

  function drawAxisRow(label, value, maxAbs, rowIdx) {
    const yRow = rowY + rowIdx * 32;
    stroke(100);
    line(barOriginX, yRow, barOriginX + barWidth, yRow);

    const len = constrain(
      map(value, -maxAbs, maxAbs, -barWidth / 2, barWidth / 2),
      -barWidth / 2,
      barWidth / 2
    );

    stroke(0, 255, 160);
    strokeWeight(4);
    line(barOriginX + barWidth / 2, yRow, barOriginX + barWidth / 2 + len, yRow);

    noStroke();
    fill(230);
    text(
      label + ': ' + nf(value, 1, 1),
      10,
      yRow - 8
    );
  }
Line-by-line explanation (5 lines)
const yRow = rowY + rowIdx * 32;
Calculates the vertical position of this bar's row, spacing each row 32 pixels apart based on its index.
line(barOriginX, yRow, barOriginX + barWidth, yRow);
Draws a plain gray baseline showing the full possible range of the gauge.
const len = constrain(map(value, -maxAbs, maxAbs, -barWidth / 2, barWidth / 2), -barWidth / 2, barWidth / 2);
Converts the raw sensor value into a pixel offset using map(), then clamps it with constrain() so the bar never overshoots its track even with extreme readings.
line(barOriginX + barWidth / 2, yRow, barOriginX + barWidth / 2 + len, yRow);
Draws the colored bar starting from the center of the track and extending left or right by the mapped length.
text(label + ': ' + nf(value, 1, 1), 10, yRow - 8);
Prints the numeric value above the bar, formatted to 1 decimal place with nf().

drawCameraPanel()

This function demonstrates the classic 'fit inside a box while preserving aspect ratio' calculation, a pattern you'll reuse anytime you display images or video at a different size than their source.

🔬 This block preserves the webcam's aspect ratio so it never looks stretched. What happens visually if you delete this entire if/else block and just use the original vw/vh values directly?

    if (vw / vh > aspectCam) {
      vw = vh * aspectCam;
    } else {
      vh = vw / aspectCam;
    }
function drawCameraPanel(x, y, w, h) {
  push();
  translate(x, y);
  noStroke();
  fill(30, 30, 50);
  rect(0, 0, w, h);

  fill(255);
  textSize(14);
  textAlign(LEFT, TOP);
  text('CAMERA', 10, 10);

  textSize(12);
  text('Live video from your webcam.', 10, 30);
  text('Allow camera access if prompted.', 10, 46);

  if (cam && cam.width > 0 && cam.height > 0) {
    // Keep aspect ratio
    let vw = w - 40;
    let vh = h - 80;
    const aspectCam = cam.width / cam.height;

    if (vw / vh > aspectCam) {
      vw = vh * aspectCam;
    } else {
      vh = vw / aspectCam;
    }

    const imgX = (w - vw) / 2;
    const imgY = (h - vh) / 2 + 8;

    push();
    // Slight tint to make it feel integrated
    tint(255, 230);
    image(cam, imgX, imgY, vw, vh);
    pop();
  } else {
    text('Waiting for camera... (check permissions)', 10, 70);
  }

  pop();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Camera Ready Check if (cam && cam.width > 0 && cam.height > 0) {

Only tries to draw the webcam image once the video stream has actually loaded and has real dimensions.

if (cam && cam.width > 0 && cam.height > 0) {
Waits until the webcam capture object exists and has non-zero size, since createCapture() loads asynchronously.
const aspectCam = cam.width / cam.height;
Calculates the video's natural width-to-height ratio so it can be preserved when resizing.
if (vw / vh > aspectCam) { vw = vh * aspectCam; } else { vh = vw / aspectCam; }
Fits the video inside the available box without stretching it - shrinking whichever dimension would otherwise distort the picture.
tint(255, 230);
Slightly reduces the video's opacity so it visually blends with the dark panel background instead of looking pasted on top.
image(cam, imgX, imgY, vw, vh);
Draws the live webcam frame at the calculated position and size.

drawAudioAnd3DPanel()

This function ties together audio analysis (p5.Amplitude), procedural noise, and an off-screen WEBGL scene composited into a 2D canvas via image() - a pattern useful anytime you want 3D graphics mixed into an otherwise 2D sketch.

🔬 barLevel mixes real mic loudness (level * 8) with Perlin noise (n * 0.3). What happens if you change level * 8 to level * 20 so even quiet sounds max out the bars? What if you delete the '+ n * 0.3' term entirely?

  for (let i = 0; i < nBars; i++) {
    const barX = meterX + (i + 0.2) * (meterWidth / nBars);
    const barW = (meterWidth / nBars) * 0.6;

    const n = noise(i * 0.2, frameCount * 0.05);
    const barLevel = level * 8 + n * 0.3;
    const barH = constrain(barLevel, 0, 1) * (meterHeight - 20);

    fill(120 + barLevel * 160, 80, 210);
    rect(barX, meterY + meterHeight - barH - 4, barW, barH);
  }
function drawAudioAnd3DPanel(x, y, w, h) {
  push();
  translate(x, y);
  noStroke();
  fill(30, 20, 40);
  rect(0, 0, w, h);

  fill(255);
  textSize(14);
  textAlign(LEFT, TOP);
  text('MICROPHONE + 3D WEBGL', 10, 10);

  textSize(12);
  text('Click / tap the canvas once to enable audio.', 10, 30);
  text('Speak into the mic to drive the visuals.', 10, 46);
  text('Use arrow keys to spin the cube, H to vibrate, M to mute mic.', 10, 62);

  let level = 0;
  if (audioStarted && !micMuted) {
    level = amp.getLevel(); // https://p5js.org/reference/#/p5.Amplitude/getLevel
  }

  // Update cube rotation based on sensors + keyboard + audio
  updateCubeRotation(level);

  // 3D cube in off-screen WEBGL buffer
  if (g3d) {
    const cubeSize = min(w, h) * 0.35;
    const hueFactor = constrain(map(level, 0, 0.3, 0, 1), 0, 1);

    g3d.push();
    g3d.clear();
    g3d.background(5, 5, 25);

    g3d.ambientLight(60);
    g3d.directionalLight(200, 200, 255, 0.25, 0.25, -1);

    g3d.noStroke();
    g3d.push();
    g3d.rotateX(cubeRotX);
    g3d.rotateY(cubeRotY);
    g3d.rotateZ(cubeRotZ);
    g3d.fill(100 + hueFactor * 155, 80, 200 - hueFactor * 120);
    g3d.box(cubeSize, cubeSize, cubeSize);
    g3d.pop();

    g3d.pop();

    imageMode(CENTER);
    image(g3d, w * 0.27, h * 0.55);
    imageMode(CORNER);
  }

  // Audio meter on the right side
  const meterWidth = w * 0.42;
  const meterHeight = h * 0.4;
  const meterX = w * 0.55;
  const meterY = h * 0.4;

  stroke(80);
  noFill();
  rect(meterX, meterY, meterWidth, meterHeight);

  const nBars = 32;
  noStroke();
  for (let i = 0; i < nBars; i++) {
    const barX = meterX + (i + 0.2) * (meterWidth / nBars);
    const barW = (meterWidth / nBars) * 0.6;

    const n = noise(i * 0.2, frameCount * 0.05);
    const barLevel = level * 8 + n * 0.3;
    const barH = constrain(barLevel, 0, 1) * (meterHeight - 20);

    fill(120 + barLevel * 160, 80, 210);
    rect(barX, meterY + meterHeight - barH - 4, barW, barH);
  }

  fill(255);
  textAlign(LEFT, TOP);
  const micStatus = audioStarted
    ? micMuted
      ? 'muted (press M)'
      : 'listening (press M to mute)'
    : 'waiting for click / tap';
  text('Mic status: ' + micStatus, meterX, meterY - 20);
  text('Audio level: ' + nf(level, 1, 3), meterX, meterY + meterHeight + 4);

  pop();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation 3D Cube Render Block g3d.box(cubeSize, cubeSize, cubeSize);

Renders the rotating, audio-colored cube into the off-screen WEBGL buffer before it's drawn onto the 2D canvas.

for-loop Audio Bar Meter Loop for (let i = 0; i < nBars; i++) {

Draws each vertical bar of the fake spectrum meter, mixing real mic level with Perlin noise for visual liveliness.

if (audioStarted && !micMuted) { level = amp.getLevel(); }
Only reads a real microphone level if audio has been started (via user gesture) and the mic isn't muted - otherwise level stays 0.
updateCubeRotation(level);
Hands the current mic loudness to the rotation helper, which factors it into how fast the cube spins.
g3d.clear();
Clears the off-screen WEBGL buffer each frame so the previous cube position doesn't leave a trail.
g3d.rotateX(cubeRotX);
Applies the current rotation angles (updated elsewhere) to the cube before drawing it.
g3d.fill(100 + hueFactor * 155, 80, 200 - hueFactor * 120);
Shifts the cube's color based on how loud the mic is - louder audio pushes the color toward a different hue.
image(g3d, w * 0.27, h * 0.55);
Draws the entire off-screen 3D render as a single image onto the main 2D canvas, centered at that position.
const n = noise(i * 0.2, frameCount * 0.05);
Samples Perlin noise using the bar's index and the current frame count, giving each bar a smoothly wobbling baseline even in silence.
const barLevel = level * 8 + n * 0.3;
Combines the real mic level (amplified 8x) with a bit of noise so the meter still looks alive when it's quiet.

drawHeader()

drawHeader() runs last in draw() so its text always sits on top of every panel - a simple but important ordering trick for overlay UI in p5.js.

function drawHeader() {
  push();
  fill(255);
  noStroke();
  textAlign(CENTER, TOP);
  textSize(18);
  text('Device Feature Playground', width / 2, 6);

  textSize(12);
  text(
    'Move mouse / touch (top-left), tilt device (top-right), allow camera + mic (bottom), use keys: arrows, M, H.',
    width / 2,
    26
  );

  // Show last key pressed
  if (lastKey) {
    textAlign(LEFT, TOP);
    text('Last key: ' + lastKey, 10, height - 20);
  }

  pop();
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Last Key Display if (lastKey) {

Only shows the 'Last key' text once a key has actually been pressed, avoiding an empty label at startup.

textAlign(CENTER, TOP);
Centers the title text horizontally around the x coordinate given to text(), anchored to its top edge.
text('Device Feature Playground', width / 2, 6);
Draws the main title centered at the top of the canvas, 6 pixels down from the top edge.
if (lastKey) {
Checks whether lastKey is a non-empty string (truthy) before drawing the 'Last key' label.

addRipple()

This tiny function is the classic 'spawn a particle' pattern: create a plain object describing one ripple's state and push it into an array that gets updated and drawn every frame elsewhere.

function addRipple(x, y) {
  ripples.push({ x, y, r: 0, alpha: 255 });
}
Line-by-line explanation (1 lines)
ripples.push({ x, y, r: 0, alpha: 255 });
Adds a new ripple object to the array using shorthand property names (x, y) plus starting radius 0 and full opacity 255.

drawRipplesInPanel()

This function demonstrates the standard 'update, then cull dead items' loop used in almost every particle system - looping backwards with splice() is the key trick that avoids skipping array elements.

🔬 This cleanup check removes fully-faded ripples from the array. What do you think would happen to performance over time if you commented this whole block out and let ripples pile up forever?

    if (r.alpha <= 0) {
      ripples.splice(i, 1);
      continue;
    }
function drawRipplesInPanel(x, y, w, h) {
  for (let i = ripples.length - 1; i >= 0; i--) {
    const r = ripples[i];
    r.r += 3;
    r.alpha -= 4;

    if (r.alpha <= 0) {
      ripples.splice(i, 1);
      continue;
    }

    // Only draw ripple if inside this panel
    if (r.x >= x && r.x <= x + w && r.y >= y && r.y <= y + h) {
      noFill();
      stroke(200, r.alpha);
      strokeWeight(2);
      ellipse(r.x, r.y, r.r * 2, r.r * 2);
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

for-loop Ripple Update & Cleanup Loop for (let i = ripples.length - 1; i >= 0; i--) {

Iterates backwards through the ripples array so items can be safely removed with splice() while looping.

for (let i = ripples.length - 1; i >= 0; i--) {
Loops from the last ripple to the first - going backwards makes it safe to remove items mid-loop without skipping the next one.
r.r += 3;
Grows the ripple's radius by 3 pixels every frame, making it expand outward.
r.alpha -= 4;
Fades the ripple's opacity by 4 every frame so it eventually becomes fully transparent.
if (r.alpha <= 0) { ripples.splice(i, 1); continue; }
Once a ripple is fully faded, removes it from the array entirely so the array doesn't grow forever.
if (r.x >= x && r.x <= x + w && r.y >= y && r.y <= y + h) {
Only actually draws this ripple if its origin point falls within the panel currently being rendered.
ellipse(r.x, r.y, r.r * 2, r.r * 2);
Draws the ripple as a growing, fading circle outline centered on where the user clicked or tapped.

inPointerPanel()

This helper hardcodes the top-left quadrant's boundaries rather than accepting panel coordinates as parameters, which keeps the calling code short but makes the function less reusable - worth noticing as a design tradeoff.

function inPointerPanel(px, py) {
  return px >= 0 && px < width / 2 && py >= 0 && py < height / 2;
}
Line-by-line explanation (1 lines)
return px >= 0 && px < width / 2 && py >= 0 && py < height / 2;
Returns true only if the given point falls inside the top-left quadrant, where the pointer panel always lives.

updateCubeRotation()

This function shows how to layer three different control sources - keyboard, device sensors, and an idle fallback - onto the same variables, with lerp() providing smooth interpolation so switching between them doesn't look jarring.

🔬 The lerp() amount 0.1 controls how quickly the cube 'catches up' to your device's real tilt. What happens visually if you raise it to 0.5 (snappier) or lower it to 0.02 (much smoother/laggier)?

  if (motionPermissionGranted) {
    const rx = radians(rotationX || 0);
    const ry = radians(rotationY || 0);
    const rz = radians(rotationZ || 0);

    if (abs(rx) + abs(ry) + abs(rz) > 0.01) {
      cubeRotX = lerp(cubeRotX, rx * 0.6, 0.1);
      cubeRotY = lerp(cubeRotY, ry * 0.6, 0.1);
      cubeRotZ = lerp(cubeRotZ, rz * 0.6, 0.1);
      return;
    }
  }
function updateCubeRotation(audioLevel) {
  // Keyboard control (arrows)
  const baseSpeed = 0.03 + audioLevel * 0.2;
  if (keyIsDown(LEFT_ARROW)) cubeRotY -= baseSpeed;
  if (keyIsDown(RIGHT_ARROW)) cubeRotY += baseSpeed;
  if (keyIsDown(UP_ARROW)) cubeRotX -= baseSpeed;
  if (keyIsDown(DOWN_ARROW)) cubeRotX += baseSpeed;

  // Device rotation (if available / granted)
  if (motionPermissionGranted) {
    const rx = radians(rotationX || 0);
    const ry = radians(rotationY || 0);
    const rz = radians(rotationZ || 0);

    if (abs(rx) + abs(ry) + abs(rz) > 0.01) {
      cubeRotX = lerp(cubeRotX, rx * 0.6, 0.1);
      cubeRotY = lerp(cubeRotY, ry * 0.6, 0.1);
      cubeRotZ = lerp(cubeRotZ, rz * 0.6, 0.1);
      return;
    }
  }

  // Idle spin if no sensor data / permission
  cubeRotY += 0.01;
  cubeRotX += 0.005;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Arrow Key Rotation if (keyIsDown(LEFT_ARROW)) cubeRotY -= baseSpeed;

Lets the player manually spin the cube with the arrow keys, sped up when the mic is loud.

conditional Device Sensor Rotation if (motionPermissionGranted) { ... }

Smoothly matches the cube's rotation to the device's real-world tilt when motion sensors are available.

const baseSpeed = 0.03 + audioLevel * 0.2;
Sets a base rotation speed that increases the louder the microphone picks up sound, linking audio volume to visual motion.
if (keyIsDown(LEFT_ARROW)) cubeRotY -= baseSpeed;
Checks if the left arrow key is currently held down (not just pressed once) and rotates the cube around the Y axis accordingly.
const rx = radians(rotationX || 0);
Converts the device's real pitch value from degrees to radians so it can be used directly as a rotation angle.
if (abs(rx) + abs(ry) + abs(rz) > 0.01) {
Only applies sensor-based rotation if there's meaningful sensor data, avoiding jitter from near-zero noise.
cubeRotX = lerp(cubeRotX, rx * 0.6, 0.1);
Smoothly interpolates the cube's current rotation toward the sensor-reported angle instead of snapping instantly, creating fluid motion.
cubeRotY += 0.01;
Falls back to a gentle automatic spin whenever there's no sensor data and no key is held - keeping the cube visually alive.

ensureAudioStarted()

Browsers require a user gesture (a click or tap) before allowing audio to play or microphone access to start - this function centralizes that logic so it can be safely called from multiple event handlers without starting audio twice.

function ensureAudioStarted() {
  if (audioStarted) return;

  if (typeof userStartAudio === 'function') {
    // https://p5js.org/reference/#/p5/userStartAudio
    userStartAudio()
      .then(() => {
        mic.start();
        audioStarted = true;
        micMuted = false;
      })
      .catch((err) => {
        console.error('Audio start failed:', err);
      });
  } else {
    // Fallback if p5.sound is not available (should not happen with our HTML)
    mic.start();
    audioStarted = true;
    micMuted = false;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Already Started Guard if (audioStarted) return;

Prevents trying to start audio more than once, since userStartAudio() only needs to run a single time.

if (audioStarted) return;
Exits immediately if audio has already been started, avoiding duplicate mic.start() calls.
userStartAudio()
Asks the browser to unlock its audio context, which most browsers block until triggered by a real user gesture like a click.
.then(() => { mic.start(); audioStarted = true; micMuted = false; })
Once the audio context is unlocked, starts the microphone stream and updates state so the rest of the sketch knows audio is live.
.catch((err) => { console.error('Audio start failed:', err); })
Logs an error if the browser refuses to unlock audio (for example, if permission was denied).

ensureSensorsActivated()

iOS Safari treats motion and orientation sensors as sensitive data requiring explicit user permission via a promise-based API, while most other browsers expose them automatically - this function bridges that difference so the rest of the sketch doesn't need to care which platform it's on.

function ensureSensorsActivated() {
  if (motionPermissionAsked) return;
  motionPermissionAsked = true;

  // DeviceMotion (acceleration, rotation)
  if (
    typeof DeviceMotionEvent !== 'undefined' &&
    typeof DeviceMotionEvent.requestPermission === 'function'
  ) {
    DeviceMotionEvent.requestPermission()
      .then((response) => {
        motionPermissionGranted = response === 'granted';
      })
      .catch((err) => {
        console.warn('DeviceMotion permission error:', err);
        motionPermissionGranted = false;
      });
  } else {
    // Other platforms: p5 will use sensors automatically if available
    motionPermissionGranted = true;
  }

  // DeviceOrientation (compass) – fire permission request if needed
  if (
    typeof DeviceOrientationEvent !== 'undefined' &&
    typeof DeviceOrientationEvent.requestPermission === 'function'
  ) {
    DeviceOrientationEvent.requestPermission().catch(() => {
      // ignore; some browsers separate permissions, but acceleration might still work
    });
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional iOS Permission Request if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {

Detects iOS Safari's special requirement to explicitly request motion sensor permission via a promise-based API.

if (motionPermissionAsked) return;
Ensures this permission prompt is only triggered once, even if the user clicks multiple times.
typeof DeviceMotionEvent.requestPermission === 'function'
Feature-detects whether the browser (mainly iOS Safari 13+) requires an explicit permission request before motion sensors will work.
motionPermissionGranted = response === 'granted';
Stores whether the user actually approved the motion sensor permission prompt.
motionPermissionGranted = true;
On platforms that don't require explicit permission, assumes sensors will work automatically if the hardware supports them.

triggerHaptics()

The Vibration API is a good example of a feature-detected browser capability - always check 'feature' in navigator (or typeof checks) before calling hardware APIs that not every device supports.

function triggerHaptics(pattern) {
  if ('vibrate' in navigator) {
    navigator.vibrate(pattern);
  }
}
Line-by-line explanation (2 lines)
if ('vibrate' in navigator) {
Checks whether the current browser/device supports the Vibration API before trying to use it, avoiding errors on unsupported devices like desktops.
navigator.vibrate(pattern);
Triggers a vibration using either a single duration in milliseconds or an array pattern of on/off durations.

mouseMoved()

mouseMoved() is a p5.js event function that fires automatically whenever the mouse moves, without needing to be called manually from draw().

function mouseMoved() {
  lastInputType = 'mouse';
}
Line-by-line explanation (1 lines)
lastInputType = 'mouse';
Records that the most recent input came from the mouse, which the pointer panel uses to pick its background tint.

mousePressed()

This function bundles several one-time setup actions (audio, sensors, haptics) into the very first user gesture, which is the standard pattern for satisfying browsers' 'must be triggered by user interaction' security rules.

function mousePressed() {
  lastInputType = 'mouse';
  lastKey = '';

  ensureAudioStarted();
  ensureSensorsActivated();
  triggerHaptics(20);

  if (inPointerPanel(mouseX, mouseY)) {
    addRipple(mouseX, mouseY);
  }
}
Line-by-line explanation (4 lines)
ensureAudioStarted();
Uses this click as the required user gesture to unlock the browser's audio context and start the microphone.
ensureSensorsActivated();
Uses the same click to request motion sensor permission on platforms (like iOS) that require it.
triggerHaptics(20);
Fires a short 20-millisecond vibration as tactile feedback for the click, on supported devices.
if (inPointerPanel(mouseX, mouseY)) { addRipple(mouseX, mouseY); }
Only spawns a ripple if the click happened inside the top-left pointer panel.

mouseDragged()

mouseDragged() fires repeatedly while the mouse button is held and moved, making it perfect for continuous effects like painting or, here, ripple trails.

function mouseDragged() {
  lastInputType = 'mouse';
  if (inPointerPanel(mouseX, mouseY)) {
    addRipple(mouseX, mouseY);
  }
}
Line-by-line explanation (1 lines)
if (inPointerPanel(mouseX, mouseY)) { addRipple(mouseX, mouseY); }
Continuously spawns new ripples while the mouse is dragged inside the pointer panel, creating a trail effect.

touchStarted()

touchStarted() is p5's touchscreen counterpart to mousePressed() - returning false from it is the standard way to prevent the browser from scrolling or zooming in response to touch gestures on the canvas.

function touchStarted() {
  lastInputType = 'touch';
  lastKey = '';

  ensureAudioStarted();
  ensureSensorsActivated();
  triggerHaptics([10, 20, 10]);

  for (const t of touches) {
    if (inPointerPanel(t.x, t.y)) {
      addRipple(t.x, t.y);
    }
  }

  // Prevent page scrolling on touch devices
  return false;
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Touch Ripple Spawn Loop for (const t of touches) {

Spawns a ripple for every active touch point that lands inside the pointer panel, supporting multi-touch.

triggerHaptics([10, 20, 10]);
Vibrates in a short pattern (vibrate-pause-vibrate) instead of a single pulse, to distinguish touch feedback from mouse click feedback.
for (const t of touches) {
Loops through every simultaneous touch point, since touchscreens can register multiple fingers at once.
return false;
Tells the browser not to perform its default touch behavior (like scrolling the page), keeping the gesture entirely inside the sketch.

touchMoved()

touchMoved() mirrors mouseDragged() for touchscreens, firing repeatedly while any finger moves - useful for continuous drawing or, here, ripple trails.

function touchMoved() {
  lastInputType = 'touch';
  for (const t of touches) {
    if (inPointerPanel(t.x, t.y)) {
      addRipple(t.x, t.y);
    }
  }
  return false;
}
Line-by-line explanation (2 lines)
for (const t of touches) { if (inPointerPanel(t.x, t.y)) { addRipple(t.x, t.y); } }
Spawns new ripples continuously as fingers move across the pointer panel, similar to mouseDragged().
return false;
Prevents the browser's default touch-scroll behavior while dragging fingers across the canvas.

keyPressed()

keyPressed() fires once per keydown in p5.js, making it the right place for discrete actions like toggling mute - compare this to keyIsDown(), used elsewhere in the sketch for continuous actions like holding an arrow key.

function keyPressed() {
  lastKey = key;

  // Haptic feedback
  if (key === 'H' || key === 'h') {
    triggerHaptics([40, 40, 40]);
  }

  // Toggle mic mute
  if (key === 'M' || key === 'm') {
    if (audioStarted && mic) {
      if (micMuted) {
        mic.start();
        micMuted = false;
      } else {
        mic.stop();
        micMuted = true;
      }
    } else if (!audioStarted) {
      // If audio wasn’t started yet, this keypress can start it
      ensureAudioStarted();
    }
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional H Key Vibration if (key === 'H' || key === 'h') { triggerHaptics([40, 40, 40]); }

Fires a triple vibration pulse whenever the H key is pressed.

conditional M Key Mute Toggle if (key === 'M' || key === 'm') { ... }

Starts or stops the microphone depending on its current mute state when the M key is pressed.

lastKey = key;
Stores whichever key was just pressed so drawHeader() can display it on screen.
if (key === 'H' || key === 'h') { triggerHaptics([40, 40, 40]); }
Checks both uppercase and lowercase 'h' (since key is case-sensitive) and triggers a vibration pattern.
if (micMuted) { mic.start(); micMuted = false; } else { mic.stop(); micMuted = true; }
Toggles the microphone stream on or off each time M is pressed, flipping the micMuted flag to match.
} else if (!audioStarted) { ensureAudioStarted(); }
If the mic hasn't been started yet at all, treats pressing M as the initial gesture to start audio instead of muting.

windowResized()

windowResized() is a p5.js event that fires automatically when the browser window changes size - pairing it with resizeCanvas() is the standard way to build a responsive full-window sketch.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window whenever it changes size, keeping the four-panel layout filling the screen.

📦 Key Variables

cam object

Holds the p5.MediaElement returned by createCapture(VIDEO), used to read and draw live webcam frames each frame.

let cam;
mic object

A p5.AudioIn instance representing the microphone input stream, started only after a user gesture.

let mic;
amp object

A p5.Amplitude analyzer connected to the mic, used to measure how loud the incoming audio is each frame.

let amp;
audioStarted boolean

Tracks whether the user has granted audio permission and the mic has actually been started.

let audioStarted = false;
micMuted boolean

Tracks whether the microphone has been manually muted via the M key, independent of audioStarted.

let micMuted = false;
g3d object

An off-screen WEBGL graphics buffer used to render the rotating 3D cube separately from the main 2D canvas.

let g3d;
cubeRotX number

Current rotation angle (radians) of the cube around the X axis, updated by keyboard, sensors, or idle spin.

let cubeRotX = 0;
cubeRotY number

Current rotation angle (radians) of the cube around the Y axis.

let cubeRotY = 0;
cubeRotZ number

Current rotation angle (radians) of the cube around the Z axis, mostly driven by device sensors.

let cubeRotZ = 0;
lastInputType string

Remembers whether 'mouse' or 'touch' was the most recent pointer input, used to tint the pointer panel.

let lastInputType = 'none';
lastKey string

Stores the most recently pressed keyboard key so it can be shown in the header text.

let lastKey = '';
ripples array

A list of ripple objects ({x, y, r, alpha}) spawned by clicks and taps, animated and removed over time.

let ripples = [];
motionPermissionAsked boolean

Guards against asking for iOS motion sensor permission more than once per session.

let motionPermissionAsked = false;
motionPermissionGranted boolean

Whether device motion/orientation permission was actually granted, used to decide whether sensor data can be trusted.

let motionPermissionGranted = false;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

BUG inPointerPanel()

This function hardcodes the top-left quadrant (0 to width/2, 0 to height/2) instead of accepting panel bounds as parameters, unlike every drawXPanel() function which takes x, y, w, h.

💡 Give it the same signature as the panel functions, e.g. inPointerPanel(px, py, x, y, w, h), so it stays correct if the panel layout ever changes.

BUG ensureSensorsActivated()

On platforms without DeviceMotionEvent.requestPermission (most non-iOS browsers, including desktops with no sensors at all), motionPermissionGranted is set to true unconditionally, so the UI reports 'Sensors active: yes' even when there is no real motion hardware.

💡 Check for the actual existence of DeviceMotionEvent/DeviceOrientationEvent (or listen for a real 'deviceorientation' event) before assuming permission was effectively granted.

PERFORMANCE drawAudioAnd3DPanel()

Several 'magic number' constants (0.35 for cube size, level * 8, 160, 0.3 noise scale, 32 bars) are scattered inline, making the visual tuning harder to find and adjust consistently.

💡 Pull these into named constants near the top of the function (or as global tunables), which also makes them easier to expose as sliders for experimentation.

FEATURE drawAudioAnd3DPanel() audio meter

The 'spectrum' bars aren't actually frequency-based - they're all driven by the same single amplitude value plus per-bar Perlin noise, so it doesn't reflect real frequency content (bass vs. treble) in the sound.

💡 Use p5.FFT and its analyze() method to get real per-frequency-bin data, so each bar represents an actual frequency band instead of noise dressed up as one.

🔄 Code Flow

Code flow showing setup, draw, drawpointerpanel, drawmotionpanel, drawaxisrow, drawcamerapanel, drawaudioand3dpanel, drawheader, addripple, drawripplesinpanel, inpointerpanel, updatecuberotation, ensureaudiostarted, ensuresensorsactivated, triggerhaptics, mousemoved, mousepressed, mousedragged, touchstarted, touchmoved, keypressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> drawpointerpanel[drawpointerpanel] draw --> drawmotionpanel[drawmotionpanel] draw --> drawaxisrow[drawaxisrow] draw --> drawcamerapanel[drawcamerapanel] draw --> draudioand3dpanel[drawaudioand3dpanel] draw --> drawheader[drawheader] drawpointerpanel --> panel-calls[Panel Calls] panel-calls --> basecolor-conditional[Background Color by Input Type] panel-calls --> touch-indicator-loop[Touch Point Indicators] drawmotionpanel --> accel-rows[Acceleration Bar Rows] drawmotionpanel --> rotation-rows[Rotation Bar Rows] drawmotionpanel --> compass-needle[Compass Needle] drawcamerapanel --> camera-ready-check[Camera Ready Check] draudioand3dpanel --> cube-render[3D Cube Render Block] draudioand3dpanel --> meter-loop[Audio Bar Meter Loop] drawheader --> lastkey-check[Last Key Display] drawripplesinpanel --> ripple-cleanup-loop[Ripple Update & Cleanup Loop] updatecuberotation --> keyboard-control[Arrow Key Rotation] updatecuberotation --> sensor-control[Device Sensor Rotation] ensureaudiostarted --> already-started-guard[Already Started Guard] ensuresensorsactivated --> ios-permission-check[iOS Permission Request] mousepressed --> ensureaudiostarted mousepressed --> ensuresensorsactivated mousepressed --> triggerhaptics[Haptic Feedback] mousepressed --> touch-ripple-loop[Touch Ripple Spawn Loop] keypressed --> haptic-key-check[H Key Vibration] keypressed --> mute-toggle[M Key Mute Toggle] windowresized --> resizeCanvas[Resize Canvas] click setup href "#fn-setup" click draw href "#fn-draw" click drawpointerpanel href "#fn-drawpointerpanel" click drawmotionpanel href "#fn-drawmotionpanel" click drawaxisrow href "#fn-drawaxisrow" click drawcamerapanel href "#fn-drawcamerapanel" click draudioand3dpanel href "#fn-drawaudioand3dpanel" click drawheader href "#fn-drawheader" click drawripplesinpanel href "#fn-drawripplesinpanel" click updatecuberotation href "#fn-updatecuberotation" click ensureaudiostarted href "#fn-ensureaudiostarted" click ensuresensorsactivated href "#fn-ensuresensorsactivated" click triggerhaptics href "#fn-triggerhaptics" click mousepressed href "#fn-mousepressed" click keypressed href "#fn-keypressed" click windowresized href "#fn-windowresized" click panel-calls href "#sub-panel-calls" click basecolor-conditional href "#sub-basecolor-conditional" click touch-indicator-loop href "#sub-touch-indicator-loop" click accel-rows href "#sub-accel-rows" click rotation-rows href "#sub-rotation-rows" click compass-needle href "#sub-compass-needle" click camera-ready-check href "#sub-camera-ready-check" click cube-render href "#sub-cube-render" click meter-loop href "#sub-meter-loop" click lastkey-check href "#sub-lastkey-check" click ripple-cleanup-loop href "#sub-ripple-cleanup-loop" click keyboard-control href "#sub-keyboard-control" click sensor-control href "#sub-sensor-control" click already-started-guard href "#sub-already-started-guard" click ios-permission-check href "#sub-ios-permission-check" click touch-ripple-loop href "#sub-touch-ripple-loop" click haptic-key-check href "#sub-haptic-key-check" click mute-toggle href "#sub-mute-toggle"

Preview

SystemTest2 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of SystemTest2 - Code flow showing setup, draw, drawpointerpanel, drawmotionpanel, drawaxisrow, drawcamerapanel, drawaudioand3dpanel, drawheader, addripple, drawripplesinpanel, inpointerpanel, updatecuberotation, ensureaudiostarted, ensuresensorsactivated, triggerhaptics, mousemoved, mousepressed, mousedragged, touchstarted, touchmoved, keypressed, windowresized
Code Flow Diagram