SystemTest1

This sketch creates a comprehensive device capability tester that displays real-time information about input handling, motion sensors, microphone input, camera feed, WebGL rendering, and haptic feedback. It demonstrates how to detect and use both desktop and mobile device capabilities, making it an excellent reference for building cross-platform interactive applications.

🧪 Try This!

Experiment with the code by making these changes:

  1. Speed up the spinning cube — Increase how fast the cube rotates by changing the angle increment—higher numbers make it spin faster
  2. Make the microphone meter more responsive — Change the smoothing factor from 0.2 to 0.8 so the meter reacts faster to sound without smoothing lag
  3. Change the test tone to a lower pitch — Lower frequencies sound deeper; change 440 Hz (A4) to 220 Hz (A3) for a bassier tone
  4. Make the mouse position circle bigger — Increase the cyan visualizer circle's diameter from 40 to 100 pixels to make it more visible
  5. Brighten the background — Change the background color from very dark (10) to medium gray (80) for a lighter appearance
  6. Make the vibration pattern longer — Add more vibration pulses by extending the pattern array—the numbers are in milliseconds
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch builds a live diagnostic dashboard that reveals what hardware and APIs your device supports. It tests microphone input with amplitude visualization, camera capture with live preview, accelerometer and gyroscope data, haptic vibration feedback, and WebGL 3D rendering. Every time you interact with the page, it displays your input in real-time—mouse position, touch coordinates, keyboard input, and more. The visualizers include a rotating 3D cube, a microphone level meter, an orientation-sensitive tilting square, and direct feedback from the Geolocation and DeviceMotion APIs.

The code is organized into discrete sections: global variables track state for each feature, setup() initializes buttons and detects browser capabilities, draw() renders the entire dashboard with six information panels (Input, Sensors, Audio, Haptics, Environment, Camera, WebGL), and helper functions handle user permissions and feature initialization. By studying this sketch you will learn how to request browser permissions for sensitive APIs, integrate p5.sound for audio input and synthesis, use createCapture() for video, access device motion and orientation, test for WebGL 2 support, and structure a data-heavy visualization that updates every frame.

⚙️ How It Works

  1. When the sketch loads, setup() detects which browser capabilities are available (DeviceMotion, DeviceOrientation, Vibration API, WebGL 2) and creates five control buttons that are permission-gated: Enable Microphone, Enable Camera, Enable Motion Sensors, Test Vibration, and Test Audio Output.
  2. Every frame, draw() clears the canvas and renders six information panels in a two-column layout: INPUT (showing mouse position, touch count, keyboard input with a cyan crosshair visualizer), SENSORS (displaying acceleration and rotation data from the device, with a rotating square that tilts based on device orientation), AUDIO (showing microphone enabled status and a green-bar level meter that responds to sound), HAPTICS (reporting vibration support), ENVIRONMENT (displaying window size and frame rate), and CAMERA (showing live video preview if enabled) plus WEBGL TEST (a 3D rotating cube).
  3. When the user clicks 'Enable Microphone', initMic() requests audio permission, creates a p5.AudioIn object to capture sound, and links it to a p5.Amplitude object that measures audio levels—the level is then smoothed with lerp() and drawn as a green rectangle that grows and shrinks in real time.
  4. When the user clicks 'Enable Camera', initCamera() requests camera permission via createCapture() with constraints for 320×240 video, then displays the live video stream in the top-right corner.
  5. When the user clicks 'Enable Motion Sensors', requestMotionPermission() uses the DeviceMotionEvent and DeviceOrientationEvent APIs to request permission (on iOS 13+) or enables motion tracking directly (on other platforms), then p5.js's built-in accelerationX, accelerationY, accelerationZ, rotationX, rotationY, rotationZ variables are displayed and used to rotate a visual square.
  6. When the user clicks 'Test Audio Output', toggleTone() creates and starts a p5.Oscillator playing a 440 Hz sine wave, toggling it on and off with each click. When clicked 'Test Vibration', doVibrate() calls navigator.vibrate() with a pattern of [100, 50, 100] milliseconds. The drawWebGLTest() helper function continuously updates and renders a rotating 3D box using WebGL, displaying it as a pre-rendered image in the bottom-right.

🎓 Concepts You'll Learn

Permission handling and async/awaitBrowser capability detectionAudio input and amplitude analysisVideo capture and displayDevice motion and orientation sensorsHaptic feedback APIWebGL 3D rendering and graphics buffersReal-time data visualization and dashboardsp5.sound library integration

📝 Code Breakdown

setup()

setup() runs once when the page loads. It detects which browser features are available (rather than assuming), creates the canvas and UI buttons, and initializes state variables. This 'feature detection' pattern is essential for cross-platform code—never assume a device has accelerometers, camera, or audio APIs.

🔬 This pattern repeats for all five buttons with different text and callbacks. What happens if you change btnStep from 26 to 50? How will the button spacing change?

  micButton = createButton('Enable Microphone');
  micButton.position(btnX, btnY0 + btnStep * 1);
  micButton.mousePressed(initMic);
function setup() {
  createCanvas(windowWidth, windowHeight);
  textFont('system-ui, sans-serif');
  textSize(14);

  // Detect feature support
  deviceMotionSupported = 'DeviceMotionEvent' in window;
  deviceOrientationSupported = 'DeviceOrientationEvent' in window;
  canVibrate = 'vibrate' in navigator;

  // Detect WebGL2 separately
  const testCanvas = document.createElement('canvas');
  hasWebGL2 = !!testCanvas.getContext('webgl2');

  // Prepare WebGL graphics buffer
  webglGfx = createGraphics(320, 240, WEBGL);

  lastMousePos = createVector(mouseX, mouseY);
  lastClickPos = createVector(-1, -1);

  // Control buttons (permission-gated features)
  const btnX = 20;
  const btnY0 = 20;
  const btnStep = 26;

  micButton = createButton('Enable Microphone');
  micButton.position(btnX, btnY0 + btnStep * 1);
  micButton.mousePressed(initMic);

  cameraButton = createButton('Enable Camera');
  cameraButton.position(btnX, btnY0 + btnStep * 2);
  cameraButton.mousePressed(initCamera);

  motionButton = createButton('Enable Motion Sensors');
  motionButton.position(btnX, btnY0 + btnStep * 3);
  motionButton.mousePressed(requestMotionPermission);

  hapticsButton = createButton('Test Vibration');
  hapticsButton.position(btnX, btnY0 + btnStep * 4);
  hapticsButton.mousePressed(doVibrate);

  soundButton = createButton('Test Audio Output');
  soundButton.position(btnX, btnY0 + btnStep * 5);
  soundButton.mousePressed(toggleTone);
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

calculation Feature Detection deviceMotionSupported = 'DeviceMotionEvent' in window;

Checks if the browser supports device motion events by testing if DeviceMotionEvent exists as a global object

calculation WebGL 2 Detection const testCanvas = document.createElement('canvas'); hasWebGL2 = !!testCanvas.getContext('webgl2');

Creates a temporary canvas and attempts to get a WebGL 2 context—the !! converts the result to a boolean

for-loop Control Button Setup micButton = createButton('Enable Microphone'); micButton.position(btnX, btnY0 + btnStep * 1); micButton.mousePressed(initMic);

Creates interactive buttons for permission-gated features; each button is positioned vertically with spacing and linked to a callback function

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire browser window; windowWidth and windowHeight are p5.js variables that auto-update
textFont('system-ui, sans-serif');
Sets the font for all text to use system UI fonts, which render consistently across devices
deviceMotionSupported = 'DeviceMotionEvent' in window;
Tests if DeviceMotionEvent exists as a property of the global window object; the 'in' operator returns true if it exists, false otherwise
const testCanvas = document.createElement('canvas');
Creates a hidden DOM canvas element (not a p5.js canvas) to test for WebGL 2 capabilities
hasWebGL2 = !!testCanvas.getContext('webgl2');
Attempts to get a WebGL 2 context from the test canvas; !! converts the result (object or null) to a boolean true/false
webglGfx = createGraphics(320, 240, WEBGL);
Creates an off-screen WebGL graphics buffer (not displayed directly) with 320×240 pixels; this buffer is used to render the 3D cube
lastMousePos = createVector(mouseX, mouseY);
Creates a p5.Vector object storing the current mouse position; vectors are easier to manipulate and display than separate x,y numbers
micButton = createButton('Enable Microphone');
Creates an HTML button with the text 'Enable Microphone' that will appear on the page
micButton.position(btnX, btnY0 + btnStep * 1);
Positions the button at pixel coordinates (20, 46) on the page—btnX is 20, btnY0 is 20, btnStep is 26, so 20 + 26*1 = 46
micButton.mousePressed(initMic);
Registers the initMic() function to be called when the user clicks this button

draw()

draw() is the heartbeat of the sketch—it runs 60 times per second and renders the entire dashboard by calculating text and shape positions dynamically based on canvas size and data state. Notice how variables like 'y' and 'ay' increment as each line of text is drawn—this is a common pattern for laying out multiple lines of text without hardcoding positions. The push()/pop() around the tilt square ensures that rotations don't affect the rest of the drawing.

🔬 The 0.2 is the 'smoothing factor'—it controls how quickly the meter responds to sound. What happens if you change 0.2 to 0.01 (very smooth) or 0.9 (very responsive)? Try predicting which will make the meter jittery and which will make it lag behind the sound.

  if (amplitude) {
    micLevelSmoothed = lerp(micLevelSmoothed, amplitude.getLevel(), 0.2);
  } else {
    micLevelSmoothed = lerp(micLevelSmoothed, 0, 0.2);
  }

🔬 This code draws two visualizers: a cyan circle that follows the mouse and a pink crosshair that marks the last click. The circle has a diameter of 40 pixels. What happens if you change 40 to 80 or 20? What if you change the crosshair line length from 10 to 30?

  stroke(0, 255, 255);
  noFill();
  ellipse(lastMousePos.x, lastMousePos.y, 40, 40);
  if (lastClickPos.x >= 0) {
    stroke(255, 0, 100);
    line(lastClickPos.x - 10, lastClickPos.y, lastClickPos.x + 10, lastClickPos.y);
    line(lastClickPos.x, lastClickPos.y - 10, lastClickPos.x, lastClickPos.y + 10);
  }
function draw() {
  background(10);
  fill(255);
  noStroke();
  textAlign(LEFT, TOP);

  textSize(20);
  text('Device Capability Tester', 20, 20);

  textSize(14);

  const col1x = 20;
  const col2x = width / 2;
  const startY = 20 + 26 * 6 + 20; // below the buttons

  // ===== INPUT SECTION =====
  let y = startY;
  text('INPUT', col1x, y);
  y += 18;
  text(`Last input type: ${lastInputType}`, col1x, y); y += 16;
  text(
    `Mouse position: ${nf(lastMousePos.x, 1, 0)}, ${nf(lastMousePos.y, 1, 0)}`,
    col1x,
    y
  ); 
  y += 16;

  const clickText =
    lastClickPos.x >= 0
      ? `${nf(lastClickPos.x, 1, 0)}, ${nf(lastClickPos.y, 1, 0)}`
      : 'none';
  text(`Last click at: ${clickText}`, col1x, y); 
  y += 16;

  text(`Touches active: ${lastTouchCount}`, col1x, y); y += 16;
  text(`Last key: ${lastKeyStr} (code ${lastKeyCode})`, col1x, y); 
  y += 24;

  // Mouse/touch visualizer
  stroke(0, 255, 255);
  noFill();
  ellipse(lastMousePos.x, lastMousePos.y, 40, 40);
  if (lastClickPos.x >= 0) {
    stroke(255, 0, 100);
    line(lastClickPos.x - 10, lastClickPos.y, lastClickPos.x + 10, lastClickPos.y);
    line(lastClickPos.x, lastClickPos.y - 10, lastClickPos.x, lastClickPos.y + 10);
  }

  // ===== SENSORS =====
  let sy = y + 10;
  fill(255);
  noStroke();
  text('SENSORS', col1x, sy); 
  sy += 18;
  text(`DeviceMotion supported: ${deviceMotionSupported}`, col1x, sy); sy += 16;
  text(
    `DeviceOrientation supported: ${deviceOrientationSupported}`,
    col1x,
    sy
  );
  sy += 16;
  text(`Motion permission granted: ${motionEnabled}`, col1x, sy); sy += 16;

  text(
    `accel X/Y/Z: ${nf(accelerationX || 0, 1, 2)}, ` +
      `${nf(accelerationY || 0, 1, 2)}, ${nf(accelerationZ || 0, 1, 2)}`,
    col1x,
    sy
  );
  sy += 16;

  text(
    `rotation X/Y/Z: ${nf(rotationX || 0, 1, 2)}, ` +
      `${nf(rotationY || 0, 1, 2)}, ${nf(rotationZ || 0, 1, 2)}`,
    col1x,
    sy
  );
  sy += 16;

  text(`deviceOrientation: ${deviceOrientation}`, col1x, sy); sy += 16;

  const heading =
    rotationZ !== null && rotationZ !== undefined
      ? ((rotationZ % 360) + 360) % 360
      : 0;
  text(`approx heading: ${nf(heading, 1, 1)}°`, col1x, sy); 
  sy += 24;

  // Orientation visual (tilting square)
  push();
  translate(col1x + 150, sy + 60);
  const tiltY = radians(rotationY || 0);
  rotate(tiltY * 0.3);
  stroke(0, 200, 255);
  noFill();
  rectMode(CENTER);
  rect(0, 0, 80, 80);
  pop();

  // ===== AUDIO / MIC =====
  let ax = col2x;
  let ay = startY;
  fill(255);
  text('AUDIO', ax, ay); 
  ay += 18;

  const hasWebAudio = !!(window.AudioContext || window.webkitAudioContext);
  text(`Web Audio supported: ${hasWebAudio}`, ax, ay); 
  ay += 16;

  text(
    `Mic enabled: ${micEnabled}` + (micError ? ` (error: ${micError})` : ''),
    ax,
    ay
  );
  ay += 16;

  text(`Audio output: ${audioOutStatus}`, ax, ay); 
  ay += 24;

  // Mic level meter
  if (amplitude) {
    micLevelSmoothed = lerp(micLevelSmoothed, amplitude.getLevel(), 0.2);
  } else {
    micLevelSmoothed = lerp(micLevelSmoothed, 0, 0.2);
  }

  const meterWidth = 200;
  const meterHeight = 20;
  noFill();
  stroke(200);
  rect(ax, ay, meterWidth, meterHeight);
  noStroke();
  const w = constrain(micLevelSmoothed * 5 * meterWidth, 0, meterWidth);
  fill(0, 200, 100);
  rect(ax, ay, w, meterHeight);
  fill(255);
  text(`Mic level`, ax, ay + meterHeight + 4);
  ay += meterHeight + 30;

  // ===== HAPTICS =====
  text('HAPTICS', ax, ay); 
  ay += 18;
  text(`navigator.vibrate supported: ${canVibrate}`, ax, ay); 
  ay += 16;
  if (lastVibrateResult) {
    text(`Last vibration: ${lastVibrateResult}`, ax, ay); 
    ay += 16;
  }
  ay += 16;

  // ===== ENVIRONMENT =====
  text('ENVIRONMENT', ax, ay); 
  ay += 18;
  text(`Window size: ${width} × ${height}`, ax, ay); ay += 16;
  text(`Display size: ${displayWidth} × ${displayHeight}`, ax, ay); ay += 16;
  text(`pixelDensity: ${pixelDensity()}`, ax, ay); ay += 16;
  text(`frameRate: ${nf(frameRate(), 1, 1)} fps`, ax, ay); ay += 24;

  // ===== CAMERA PREVIEW (top-right) =====
  const vw = 320;
  const vh = 240;
  const camX = width - vw - 20;
  const camY = 20;

  if (video && cameraEnabled) {
    image(video, camX, camY, vw, vh);
    noFill();
    stroke(255);
    rect(camX, camY, vw, vh);
    noStroke();
    fill(255);
    text('Camera preview', camX, camY + vh + 4);
  } else {
    fill(60);
    stroke(120);
    rect(camX, camY, vw, vh);
    noStroke();
    fill(255);
    text('Camera not enabled', camX + 10, camY + 10);
    if (cameraError) {
      text(`Error: ${cameraError}`, camX + 10, camY + 26);
    }
  }

  // ===== WEBGL TEST (bottom-right) =====
  drawWebGLTest();
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Input Section Rendering text(`Last input type: ${lastInputType}`, col1x, y); y += 16;

Displays the most recent input event (mouse, touch, or keyboard) and increments the y position for the next line of text

calculation Mouse Position Circle ellipse(lastMousePos.x, lastMousePos.y, 40, 40);

Draws a cyan circle at the current mouse position to provide visual feedback of cursor location

conditional Click Position Crosshair if (lastClickPos.x >= 0) { stroke(255, 0, 100); line(lastClickPos.x - 10, lastClickPos.y, lastClickPos.x + 10, lastClickPos.y); line(lastClickPos.x, lastClickPos.y - 10, lastClickPos.x, lastClickPos.y + 10); }

If a click has occurred (lastClickPos.x >= 0), draws a pink crosshair at the click location

calculation Microphone Level Meter if (amplitude) { micLevelSmoothed = lerp(micLevelSmoothed, amplitude.getLevel(), 0.2); } else { micLevelSmoothed = lerp(micLevelSmoothed, 0, 0.2); }

Updates the smoothed microphone level by interpolating toward the current amplitude reading—creates a responsive but not jittery meter

calculation Orientation Tilt Visualization const tiltY = radians(rotationY || 0); rotate(tiltY * 0.3);

Converts device rotation Y (pitch) from degrees to radians, scales it down by 0.3, and rotates a square to visualize device tilt

conditional Camera Preview Display if (video && cameraEnabled) { image(video, camX, camY, vw, vh); } else { fill(60); stroke(120); rect(camX, camY, vw, vh); }

If camera is enabled and video is available, displays the live feed; otherwise shows a placeholder gray rectangle

background(10);
Fills the entire canvas with a dark gray (RGB value 10), clearing the previous frame and preventing motion trails
const col1x = 20;
Sets the x-coordinate for the left column of text to 20 pixels from the left edge
const col2x = width / 2;
Sets the x-coordinate for the right column to the middle of the canvas, creating a two-column layout
const startY = 20 + 26 * 6 + 20;
Calculates where the data display should start: 20 (top margin) + 26*6 (five buttons at 26px spacing) + 20 (gap) = 196 pixels down
text(`Last input type: ${lastInputType}`, col1x, y); y += 16;
Draws the input type at position (col1x, y), then increments y by 16 to position the next line below
text(`Mouse position: ${nf(lastMousePos.x, 1, 0)}, ${nf(lastMousePos.y, 1, 0)}`, col1x, y);
Displays the mouse position with nf() formatting: nf(number, digits before decimal, digits after decimal) - here 1 digit total, 0 decimals
stroke(0, 255, 255);
Sets the drawing color for lines and outlines to cyan (RGB 0, 255, 255)
ellipse(lastMousePos.x, lastMousePos.y, 40, 40);
Draws a circle at the last mouse position with a diameter of 40 pixels, centered on the mouse
if (lastClickPos.x >= 0) {
Checks if a click has occurred—lastClickPos is initialized to (-1, -1), so this is true only after the first click
micLevelSmoothed = lerp(micLevelSmoothed, amplitude.getLevel(), 0.2);
Smooths the microphone level using lerp (linear interpolation): move 20% of the way from the old level toward the current level each frame
const w = constrain(micLevelSmoothed * 5 * meterWidth, 0, meterWidth);
Calculates the width of the green meter rectangle: amplifies the level by 5×, multiplies by meter width, then clamps to 0–meterWidth pixels
fill(0, 200, 100); rect(ax, ay, w, meterHeight);
Fills with green (RGB 0, 200, 100) and draws a rectangle from (ax, ay) with width w and height meterHeight—the green bar grows as volume increases
const tiltY = radians(rotationY || 0);
Converts the device's Y rotation (in degrees) to radians using radians(); || 0 provides a fallback if rotationY is undefined
rotate(tiltY * 0.3);
Rotates the coordinate system (and everything drawn after) by the tilt angle scaled down to 30%—makes the visual response smoother
if (video && cameraEnabled) {
Checks both that the video object exists AND that cameraEnabled is true before trying to display the camera feed
image(video, camX, camY, vw, vh);
Draws the video stream (captured from the camera) at position (camX, camY) scaled to 320×240 pixels
drawWebGLTest();
Calls the drawWebGLTest() helper function to render the 3D cube

drawWebGLTest()

drawWebGLTest() demonstrates WebGL 3D rendering by using a separate off-screen graphics buffer (webglGfx) to render a 3D scene, then displaying that buffer as a 2D image on the main canvas. This 'deferred rendering' approach is common in interactive graphics—it isolates expensive 3D rendering from the main display loop and makes it easy to show the result alongside 2D text. The ambient and directional lights give the cube visual depth without requiring complex materials.

🔬 These two lines control the spinning motion with different multipliers: 0.7 for Y (vertical axis) and 0.4 for X (horizontal axis). What happens if you change both to 0.1? Or swap them so X is 0.7 and Y is 0.4? How does it change the visual effect?

  webglGfx.rotateY(angle * 0.7);
  webglGfx.rotateX(angle * 0.4);
function drawWebGLTest() {
  if (!webglGfx) return;

  const w = 320;
  const h = 240;
  const x = width - w - 20;
  const y = height - h - 20;

  webglGfx.push();
  webglGfx.clear();
  webglGfx.background(5);
  webglGfx.noStroke();
  webglGfx.ambientLight(80);
  webglGfx.directionalLight(255, 255, 255, 0.5, 0.5, -1);
  webglGfx.rotateY(angle * 0.7);
  webglGfx.rotateX(angle * 0.4);
  webglGfx.normalMaterial();
  webglGfx.box(80);
  webglGfx.pop();

  angle += 0.01;

  image(webglGfx, x, y, w, h);
  noFill();
  stroke(255);
  rect(x, y, w, h);
  noStroke();
  fill(255);
  text(`WebGL cube (WebGL2: ${hasWebGL2})`, x, y - 18);
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Guard Clause if (!webglGfx) return;

Exits the function early if webglGfx was not initialized, preventing errors if WebGL setup failed

calculation 3D Box Rendering webglGfx.rotateY(angle * 0.7); webglGfx.rotateX(angle * 0.4); webglGfx.normalMaterial(); webglGfx.box(80);

Rotates the coordinate system on two axes and draws a box, using normalMaterial() for flat shading that responds to lighting

calculation Angle Update angle += 0.01;

Increments the rotation angle each frame, causing continuous spinning motion

calculation Display WebGL Buffer image(webglGfx, x, y, w, h);

Draws the off-screen WebGL graphics buffer onto the main canvas at position (x, y)

if (!webglGfx) return;
If webglGfx is null or undefined, exit immediately—prevents errors from trying to render a non-existent graphics buffer
const w = 320;
Declares the width of the rendered cube preview as 320 pixels
const x = width - w - 20;
Positions the cube 20 pixels from the right edge: width (full canvas width) minus 320 (cube width) minus 20 (margin)
webglGfx.push();
Saves the current transformation matrix (translation, rotation, scale) so changes don't affect other drawings
webglGfx.clear();
Clears the off-screen WebGL buffer, removing the previous frame
webglGfx.background(5);
Fills the WebGL buffer with a very dark gray (RGB 5) as the background
webglGfx.ambientLight(80);
Adds ambient (background) lighting at brightness 80, so all surfaces are visible even in shadow
webglGfx.directionalLight(255, 255, 255, 0.5, 0.5, -1);
Creates a white directional light (like sunlight) coming from direction (0.5, 0.5, -1), shining down and toward the viewer
webglGfx.rotateY(angle * 0.7);
Rotates the coordinate system around the Y-axis (vertical) by angle × 0.7 radians—slower than the X rotation for visual effect
webglGfx.rotateX(angle * 0.4);
Rotates the coordinate system around the X-axis (horizontal) by angle × 0.4 radians—slower than Y, creating a wobbling spin
webglGfx.normalMaterial();
Applies normal material shading, which colors surfaces based on their orientation to the light—creates 3D depth perception
webglGfx.box(80);
Draws a 3D box with side length 80 pixels at the origin; the box will rotate based on the rotateX/Y calls above
webglGfx.pop();
Restores the transformation matrix that was saved by push(), so subsequent draws are not affected by the rotations
angle += 0.01;
Increments angle by 0.01 radians each frame—at 60 fps, this creates smooth rotation
image(webglGfx, x, y, w, h);
Draws the rendered WebGL buffer (the 3D cube) onto the main 2D canvas at position (x, y) with size (w, h)
text(`WebGL cube (WebGL2: ${hasWebGL2})`, x, y - 18);
Displays a label above the cube preview, showing whether the device supports WebGL 2

mouseMoved()

mouseMoved() is a p5.js callback that runs automatically whenever the user moves the mouse without clicking. It's one of several input event handlers that update global state variables so draw() can display them on every frame.

function mouseMoved() {
  lastInputType = 'mouse move';
  lastMousePos.set(mouseX, mouseY);
}
Line-by-line explanation (2 lines)
lastInputType = 'mouse move';
Records that the most recent input was a mouse movement, so draw() can display this on the dashboard
lastMousePos.set(mouseX, mouseY);
Updates the lastMousePos vector to the current mouse coordinates using set() method—this stores the position for display and visualization

mousePressed()

mousePressed() is a p5.js callback fired when the user clicks the mouse. Unlike mouseMoved(), which fires continuously, mousePressed() fires once per click. This sketch uses it to capture and display the click location separately from the current mouse position.

function mousePressed() {
  lastInputType = 'mouse press';
  lastMousePos.set(mouseX, mouseY);
  lastClickPos.set(mouseX, mouseY);
}
Line-by-line explanation (3 lines)
lastInputType = 'mouse press';
Records that the most recent input was a mouse click
lastMousePos.set(mouseX, mouseY);
Updates the current mouse position at the moment of the click
lastClickPos.set(mouseX, mouseY);
Records the click position separately so draw() can draw a persistent crosshair at this location

touchStarted()

touchStarted() is a p5.js callback that fires when a finger first touches the screen. The global touches array contains all active touches. This sketch records the first touch as if it were a mouse click, enabling the same visualizers for both input types.

function touchStarted() {
  lastInputType = 'touch start';
  lastTouchCount = touches.length;
  if (touches.length > 0) {
    lastMousePos.set(touches[0].x, touches[0].y);
    lastClickPos.set(touches[0].x, touches[0].y);
  }
  return false; // prevent scroll
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Touch Position Capture if (touches.length > 0) { lastMousePos.set(touches[0].x, touches[0].y); lastClickPos.set(touches[0].x, touches[0].y); }

If at least one finger is touching the screen, records the first touch's position for display

lastInputType = 'touch start';
Records that a touch event has begun
lastTouchCount = touches.length;
Stores how many fingers are currently touching—useful for detecting multi-touch gestures
if (touches.length > 0) {
Checks if there is at least one touch before trying to access touches[0], preventing errors
return false;
Returning false prevents the browser's default touch behavior (scrolling, zooming), allowing the sketch to handle touches instead

touchMoved()

touchMoved() is a p5.js callback that fires every frame while fingers are on the screen. It allows the sketch to track touch motion in real-time and display it alongside mouse input.

function touchMoved() {
  lastInputType = 'touch move';
  lastTouchCount = touches.length;
  if (touches.length > 0) {
    lastMousePos.set(touches[0].x, touches[0].y);
  }
  return false;
}
Line-by-line explanation (4 lines)
lastInputType = 'touch move';
Records that the most recent input was a touch movement
lastTouchCount = touches.length;
Updates the count of active fingers, which may have changed since touchStarted()
if (touches.length > 0) { lastMousePos.set(touches[0].x, touches[0].y); }
Updates the mouse position to follow the first finger as it moves, so the cyan visualizer circle tracks the touch
return false;
Prevents default scrolling/zooming behavior

touchEnded()

touchEnded() fires when a finger lifts off the screen. By updating lastTouchCount, it allows draw() to display the correct number of active touches.

function touchEnded() {
  lastTouchCount = touches.length;
}
Line-by-line explanation (1 lines)
lastTouchCount = touches.length;
Updates the touch count when a finger leaves the screen—touches.length decreases as fingers are lifted

keyPressed()

keyPressed() is a p5.js callback fired when any key is pressed. This sketch records both the human-readable character (key) and the numeric code (keyCode) for display on the dashboard.

function keyPressed() {
  lastInputType = 'keyboard';
  lastKeyStr = key;
  lastKeyCode = keyCode;
}
Line-by-line explanation (3 lines)
lastInputType = 'keyboard';
Records that the most recent input was a keypress
lastKeyStr = key;
Stores the character of the key that was pressed (e.g., 'a', 'Enter', ' ') in the p5.js key variable
lastKeyCode = keyCode;
Stores the numeric keyCode of the pressed key (e.g., 65 for 'a', 13 for 'Enter') in the p5.js keyCode variable

initMic()

initMic() demonstrates permission-based feature initialization in modern browsers. It uses async/await to handle asynchronous permission requests, error handling with try-catch to gracefully handle denial, and conditional creation to avoid duplicate objects. This pattern is essential for microphone, camera, and sensor features that require explicit user permission.

async function initMic() {
  try {
    micError = '';
    await userStartAudio(); // required by browser autoplay rules
    if (!mic) {
      mic = new p5.AudioIn();
    }
    mic.start(
      () => {
        amplitude = new p5.Amplitude();
        amplitude.setInput(mic);
        micEnabled = true;
      },
      err => {
        micError = err && err.message ? err.message : 'mic start failed';
        micEnabled = false;
      }
    );
  } catch (e) {
    micError = e && e.message ? e.message : 'audio permission denied';
    micEnabled = false;
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation User Audio Permission await userStartAudio();

Requests browser permission to access audio; required by autoplay policy on many modern browsers

conditional Microphone Object Creation if (!mic) { mic = new p5.AudioIn(); }

Creates a p5.AudioIn object only if one doesn't exist, avoiding redundant creation on multiple button clicks

calculation Mic Start Callback mic.start( () => { amplitude = new p5.Amplitude(); amplitude.setInput(mic); micEnabled = true; }, err => { micError = err && err.message ? err.message : 'mic start failed'; micEnabled = false; } );

Attempts to start microphone input; on success, creates an Amplitude analyzer; on error, stores the error message

calculation Try-Catch Error Handling } catch (e) { micError = e && e.message ? e.message : 'audio permission denied'; micEnabled = false; }

Catches any exceptions (e.g., permission denied) and stores a human-readable error message for display

async function initMic() {
Declares an async function, allowing use of await for asynchronous operations like permission requests
micError = '';
Clears any previous error message before attempting a new microphone initialization
await userStartAudio();
Waits for the browser to grant audio permission; userStartAudio() is a p5.sound function that handles the browser's autoplay policy
if (!mic) {
Checks if a mic object already exists to avoid creating duplicates on repeated clicks
mic = new p5.AudioIn();
Creates a new microphone input object from the p5.sound library
mic.start(
Begins microphone input; takes two callbacks: success handler and error handler
amplitude = new p5.Amplitude();
Creates an Amplitude analyzer object in the success callback
amplitude.setInput(mic);
Connects the amplitude analyzer to the microphone so it can measure audio levels
micEnabled = true;
Sets the flag to true so draw() knows to display the meter
micError = err && err.message ? err.message : 'mic start failed';
Extracts the error message if it exists, otherwise defaults to 'mic start failed'
} catch (e) {
Catches any exception thrown during async operations
micError = e && e.message ? e.message : 'audio permission denied';
Stores the error message for display; uses a fallback if e.message doesn't exist

toggleTone()

toggleTone() demonstrates audio synthesis with p5.sound: creating an oscillator, setting frequency and amplitude, and toggling playback on and off. The oscillator object persists across button clicks, so you only create it once but toggle it many times—a common pattern for managing expensive resources.

async function toggleTone() {
  try {
    await userStartAudio();
    if (!osc) {
      osc = new p5.Oscillator('sine');
      osc.freq(440);
      osc.amp(0.3);
    }
    if (!oscOn) {
      osc.start();
      oscOn = true;
      audioOutStatus = 'tone playing (440 Hz)';
    } else {
      osc.stop();
      oscOn = false;
      audioOutStatus = 'stopped';
    }
  } catch (e) {
    audioOutStatus = 'error starting audio';
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

conditional Oscillator Creation if (!osc) { osc = new p5.Oscillator('sine'); osc.freq(440); osc.amp(0.3); }

Creates the oscillator object only once; subsequent clicks toggle it on/off without recreating it

conditional Tone Start/Stop Toggle if (!oscOn) { osc.start(); oscOn = true; audioOutStatus = 'tone playing (440 Hz)'; } else { osc.stop(); oscOn = false; audioOutStatus = 'stopped'; }

Toggles the oscillator on and off with each button click; updates status for display

async function toggleTone() {
Declares an async function to handle audio permission and oscillator control
await userStartAudio();
Ensures audio context is active before creating/starting the oscillator
if (!osc) {
Checks if an oscillator has already been created
osc = new p5.Oscillator('sine');
Creates a sine wave oscillator from p5.sound
osc.freq(440);
Sets the frequency to 440 Hz, which is the musical note A4
osc.amp(0.3);
Sets the amplitude (volume) to 0.3, a safe intermediate level
if (!oscOn) {
Checks if the oscillator is currently off
osc.start();
Starts playing the tone
oscOn = true;
Sets the flag to true so the next click will stop it
audioOutStatus = 'tone playing (440 Hz)';
Updates the status message for display on the dashboard
} else { osc.stop();
If already playing, stops the tone
oscOn = false;
Sets the flag to false so the next click will start it

initCamera()

initCamera() demonstrates camera access in p5.js using createCapture(), which uses the browser's getUserMedia API under the hood. The constraints object specifies the ideal camera capabilities (resolution, front vs. back), and the callback confirms when the stream is active. The video element is hidden and displayed manually on the canvas at a specific position and size.

function initCamera() {
  if (cameraEnabled || video) return;
  cameraError = '';

  const constraints = {
    video: {
      width: { ideal: 640 },
      height: { ideal: 480 },
      facingMode: 'user'
    },
    audio: false
  };

  video = createCapture(
    constraints,
    () => {
      cameraEnabled = true;
    }
  );
  video.size(320, 240);
  video.hide();
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Prevent Duplicate Camera if (cameraEnabled || video) return;

Exits early if camera is already initialized, preventing attempts to open it multiple times

calculation Camera Constraints const constraints = { video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: 'user' }, audio: false };

Specifies camera settings: ideal resolution and front-facing camera; audio is disabled

calculation Video Capture Setup video = createCapture( constraints, () => { cameraEnabled = true; } );

Creates a video capture element with the specified constraints; callback sets cameraEnabled flag on success

if (cameraEnabled || video) return;
Exits the function if camera is already enabled or if a video object exists, preventing duplicate initialization
cameraError = '';
Clears any previous error message
const constraints = {
Defines an object specifying camera capabilities requested from the device
width: { ideal: 640 },
Asks for 640 pixels wide; 'ideal' means the browser will try but isn't required
facingMode: 'user'
Requests the front-facing camera (used for video calls); 'environment' would request the back camera
audio: false
Explicitly requests no audio from the camera stream, only video
video = createCapture(
Starts a video stream and creates a hidden video element that can be drawn on the canvas
() => {
A callback function that runs when the camera stream is ready
cameraEnabled = true;
Sets the flag so draw() knows the camera is ready
video.size(320, 240);
Resizes the internal video resolution to 320×240 for performance (smaller streams are faster)
video.hide();
Hides the default video element (p5.js creates one) so we can display it ourselves using image()

requestMotionPermission()

requestMotionPermission() handles cross-platform differences in how devices expose motion sensors. iOS 13+ requires explicit permission via requestPermission(), while Android and older iOS platforms either don't require it or don't support the API. The nested if-else structure handles all these cases, ensuring the sketch works everywhere.

async function requestMotionPermission() {
  try {
    if (
      typeof DeviceMotionEvent !== 'undefined' &&
      typeof DeviceMotionEvent.requestPermission === 'function'
    ) {
      const response = await DeviceMotionEvent.requestPermission();
      motionEnabled = response === 'granted';
    } else if (
      typeof DeviceOrientationEvent !== 'undefined' &&
      typeof DeviceOrientationEvent.requestPermission === 'function'
    ) {
      const response = await DeviceOrientationEvent.requestPermission();
      motionEnabled = response === 'granted';
    } else {
      // Other platforms: events are either available or not; no extra permission method
      motionEnabled = true;
    }
  } catch (e) {
    motionEnabled = false;
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

conditional DeviceMotion Permission Request if ( typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function' ) { const response = await DeviceMotionEvent.requestPermission(); motionEnabled = response === 'granted'; }

On iOS 13+, requests explicit permission for device motion (accelerometer); sets flag based on response

conditional DeviceOrientation Fallback } else if ( typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function' ) { const response = await DeviceOrientationEvent.requestPermission(); motionEnabled = response === 'granted'; }

If DeviceMotion isn't available, attempts DeviceOrientation permission; handles different iOS versions

conditional Generic Platform Support } else { // Other platforms: events are either available or not; no extra permission method motionEnabled = true; }

On Android and other platforms, motion events don't require explicit permission—just enable if available

async function requestMotionPermission() {
Declares an async function to handle permission requests for device motion sensors
if ( typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function' ) {
Checks if DeviceMotionEvent exists AND if it has a requestPermission method (iOS 13+ only)
const response = await DeviceMotionEvent.requestPermission();
Requests permission and waits for the user's response
motionEnabled = response === 'granted';
Sets motionEnabled to true only if the response is 'granted'
} else if ( typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function' ) {
If DeviceMotionEvent.requestPermission doesn't exist, tries DeviceOrientationEvent (different iOS versions)
} else { motionEnabled = true;
On platforms without requestPermission (Android, older iOS), assume events are available if the API exists
} catch (e) { motionEnabled = false;
If any error occurs during permission request, disable motion (user denied, or API unavailable)

doVibrate()

doVibrate() demonstrates the Vibration API, a simple browser feature that makes the device vibrate. The pattern array alternates between vibration durations and pause durations. This is useful for haptic feedback in games and interactive apps.

function doVibrate() {
  if (navigator.vibrate) {
    const ok = navigator.vibrate([100, 50, 100]);
    lastVibrateResult = ok ? 'vibration requested' : 'vibration not allowed';
  } else {
    lastVibrateResult = 'vibrate() not supported';
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Vibrate API Check if (navigator.vibrate) {

Checks if the device supports the Vibration API before attempting to vibrate

calculation Vibration Pattern const ok = navigator.vibrate([100, 50, 100]);

Requests a vibration pattern: vibrate 100ms, pause 50ms, vibrate 100ms; returns true/false for success

if (navigator.vibrate) {
Checks if the device has navigator.vibrate available (most mobile devices do)
const ok = navigator.vibrate([100, 50, 100]);
Sends a vibration pattern: [100ms vibrate, 50ms pause, 100ms vibrate]; the function returns true if the pattern was sent
lastVibrateResult = ok ? 'vibration requested' : 'vibration not allowed';
Sets the status message based on whether the vibration was accepted (true) or blocked (false)
} else { lastVibrateResult = 'vibrate() not supported';
If navigator.vibrate doesn't exist, sets a message indicating the API isn't available

windowResized()

windowResized() is a p5.js callback that fires automatically when the browser window is resized. Calling resizeCanvas() ensures the canvas stretches to fill the new window size, which is especially important on mobile devices where screen rotation changes dimensions.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the current browser window dimensions, ensuring the canvas fills the screen after resize

📦 Key Variables

lastInputType string

Stores the type of the most recent input event (e.g., 'mouse move', 'touch start', 'keyboard') for display on the dashboard

let lastInputType = 'none';
lastMousePos p5.Vector

A vector storing the current mouse or touch position; updated every frame by mouseMoved() and touch handlers for visualization

let lastMousePos = createVector(0, 0);
lastClickPos p5.Vector

A vector storing the position of the last click or touch press; used to draw a persistent crosshair on the canvas

let lastClickPos = createVector(-1, -1);
lastTouchCount number

Stores the number of fingers currently touching the screen; updated by touch handlers

let lastTouchCount = 0;
lastKeyStr string

Stores the character of the last key pressed (e.g., 'a', 'Enter')

let lastKeyStr = 'none';
lastKeyCode number

Stores the numeric key code of the last key pressed (e.g., 65 for 'a', 13 for 'Enter')

let lastKeyCode = -1;
deviceMotionSupported boolean

Stores whether the browser supports DeviceMotionEvent (accelerometer data)

let deviceMotionSupported = false;
deviceOrientationSupported boolean

Stores whether the browser supports DeviceOrientationEvent (gyroscope/compass data)

let deviceOrientationSupported = false;
motionEnabled boolean

Stores whether the user has granted permission to access motion sensors (iOS 13+)

let motionEnabled = false;
mic p5.AudioIn object

Stores the microphone input object from p5.sound; created when user clicks 'Enable Microphone'

let mic;
amplitude p5.Amplitude object

Stores an amplitude analyzer connected to the microphone; measures audio levels for the meter visualization

let amplitude;
micEnabled boolean

Stores whether microphone input is active and ready to measure

let micEnabled = false;
micError string

Stores error messages from failed microphone initialization (e.g., 'permission denied')

let micError = '';
micLevelSmoothed number

Stores the smoothed microphone amplitude level (0–1 range); updated each frame using lerp() to avoid jitter

let micLevelSmoothed = 0;
osc p5.Oscillator object

Stores the audio oscillator object from p5.sound; creates the test tone when user clicks 'Test Audio Output'

let osc;
oscOn boolean

Stores whether the oscillator is currently playing

let oscOn = false;
audioOutStatus string

Stores a human-readable status message about audio output (e.g., 'tone playing (440 Hz)', 'stopped')

let audioOutStatus = 'idle';
video p5.Capture object

Stores the video stream object from createCapture(); contains live camera feed data

let video;
cameraEnabled boolean

Stores whether the camera stream is active and ready to display

let cameraEnabled = false;
cameraError string

Stores error messages from failed camera initialization (e.g., 'camera not found', 'permission denied')

let cameraError = '';
webglGfx p5.Graphics (WebGL)

Stores an off-screen WebGL graphics buffer used to render the 3D rotating cube

let webglGfx;
angle number

Stores the rotation angle for the 3D cube; incremented each frame to create continuous spinning

let angle = 0;
hasWebGL2 boolean

Stores whether the device supports WebGL 2 (a more modern version with more features)

let hasWebGL2 = false;
canVibrate boolean

Stores whether the device supports the Vibration API (navigator.vibrate)

let canVibrate = false;
lastVibrateResult string

Stores the result message from the last vibration attempt (e.g., 'vibration requested', 'vibrate() not supported')

let lastVibrateResult = '';
micButton, cameraButton, motionButton, hapticsButton, soundButton HTML button elements

Store references to the interactive buttons so their position and behavior can be set in setup()

let micButton, cameraButton, motionButton, hapticsButton, soundButton;

🔧 Potential Improvements (7)

Here are some ways this code could be enhanced:

BUG initCamera()

If the user denies camera permission, cameraError is never set, so the error state is not displayed

💡 Add error handling to createCapture: pass a second callback for permission errors: video = createCapture(constraints, successCb, errorCb) or use a try-catch wrapper

PERFORMANCE draw()

The layout positions (col1x, col2x, startY, y, sy, ax, ay) are recalculated every frame even though they never change

💡 Move these position constants to setup() or make them global variables to avoid recalculation 60 times per second

STYLE initMic(), toggleTone(), requestMotionPermission()

Error messages use fallback ternary chains: 'err && err.message ? err.message : fallback'; this is hard to read and fragile if err is null

💡 Use optional chaining: 'err?.message || fallback' for cleaner, more modern code

FEATURE draw() - CAMERA PREVIEW section

The camera preview shows raw video without any overlays or interaction; users can't see what they're recording

💡 Add a crosshair, recording timer, or ability to capture a frame and save it; add text overlays like 'Tap to capture'

FEATURE draw() - SENSORS section

Motion data (accelerationX, rotationY, etc.) is displayed as raw numbers; beginners don't know what the values mean

💡 Add visual indicators for magnitude (bars or colors), or interpret the data (e.g., 'Device is upright' if rotationX < 30°)

BUG draw() - Orientation visual

The tilt square only uses rotationY; ignoring rotationX and rotationZ gives incomplete visualization

💡 Apply both rotateX and rotateY to the square (in push/pop block) to show full 3D device orientation

PERFORMANCE draw() - nf() calls

nf() is called many times per frame to format numbers for display; this is a built-in function call overhead

💡 Cache formatted values or use template literals with .toFixed() instead of nf() for non-critical displays

🔄 Code Flow

Code flow showing setup, draw, drawwebgltest, mousemoved, mousepressed, touchstarted, touchmoved, touchended, keypressed, initmac, toggletone, initcamera, requestmotionpermission, dovibrate, windowresized

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

graph TD start[Start] --> setup[setup] setup --> feature-detection[Feature Detection] setup --> webgl2-test[WebGL 2 Detection] setup --> button-creation-loop[Control Button Setup] setup --> draw[draw loop] click setup href "#fn-setup" click feature-detection href "#sub-feature-detection" click webgl2-test href "#sub-webgl2-test" click button-creation-loop href "#sub-button-creation-loop" draw --> input-display[Input Section Rendering] draw --> mouse-visualizer[Mouse Position Circle] draw --> click-crosshair[Click Position Crosshair] draw --> mic-level-meter[Microphone Level Meter] draw --> orientation-tilt[Orientation Tilt Visualization] draw --> camera-conditional[Camera Preview Display] draw --> guard-check[Guard Clause] draw --> drawwebgltest[drawWebGLTest] click draw href "#fn-draw" click input-display href "#sub-input-display" click mouse-visualizer href "#sub-mouse-visualizer" click click-crosshair href "#sub-click-crosshair" click mic-level-meter href "#sub-mic-level-meter" click orientation-tilt href "#sub-orientation-tilt" click camera-conditional href "#sub-camera-conditional" click guard-check href "#sub-guard-check" click drawwebgltest href "#fn-drawwebgltest" drawwebgltest --> guard-check[Guard Check] drawwebgltest --> 3d-rendering[3D Box Rendering] drawwebgltest --> angle-increment[Angle Update] drawwebgltest --> display-image[Display WebGL Buffer] click guard-check href "#sub-guard-check" click 3d-rendering href "#sub-3d-rendering" click angle-increment href "#sub-angle-increment" click display-image href "#sub-display-image"

Preview

SystemTest1 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of SystemTest1 - Code flow showing setup, draw, drawwebgltest, mousemoved, mousepressed, touchstarted, touchmoved, touchended, keypressed, initmac, toggletone, initcamera, requestmotionpermission, dovibrate, windowresized
Code Flow Diagram