AI Wave Function Simulator - Physics Interference Visualization Explore wave interference physics!

This sketch creates an interactive physics visualization of wave interference patterns generated by two point sources. Users can drag the sources around the canvas, adjust the frequency with a slider, and watch as the waves interfere constructively and destructively, creating a dynamic blue interference pattern that updates in real-time.

๐ŸŽ“ Concepts You'll Learn

Wave physicsInterference patternsPhase calculationPixel manipulationInteractive draggingFrequency controlOff-screen renderingDistance calculationTrigonometric functionsConstrain and mapping

๐Ÿ”„ Code Flow

Code flow showing setup, draw, createfieldgraphics, initsources, setupui, updatetime, toggleplaypause, renderfield, drawsources, mousepressed, mousedragged, mousereleased, insidecanvas, windowresized

๐Ÿ’ก Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> frequency-update[Frequency Slider Update] draw --> render-pipeline[Render Pipeline] draw --> updatetime[Update Time] draw --> drawsources[Draw Sources] frequency-update --> frequency-row[Frequency Control Row] frequency-row --> frequency-update click frequency-update href "#sub-frequency-update" click frequency-row href "#sub-frequency-row" render-pipeline --> dimension-calc[Scaled Dimensions] render-pipeline --> pixel-loop[Nested Pixel Loop] pixel-loop --> distance-calc[Distance Calculations] distance-calc --> phase-calc[Phase Calculation] phase-calc --> interference[Wave Interference] interference --> pixel-color[Pixel Color Assignment] pixel-color --> render-pipeline click render-pipeline href "#sub-render-pipeline" click dimension-calc href "#sub-dimension-calc" click pixel-loop href "#sub-pixel-loop" click distance-calc href "#sub-distance-calc" click phase-calc href "#sub-phase-calc" click interference href "#sub-interference" click pixel-color href "#sub-pixel-color" updatetime --> time-delta[Time Delta Calculation] time-delta --> state-toggle[Play State Toggle] state-toggle --> updatetime click time-delta href "#sub-time-delta" click state-toggle href "#sub-state-toggle" drawsources --> circle-drawing[Source Circle Drawing] drawsources --> label-drawing[Source Label Drawing] circle-drawing --> drawsources label-drawing --> drawsources click drawsources href "#fn-drawsources" click circle-drawing href "#sub-circle-drawing" click label-drawing href "#sub-label-drawing" setup --> createfieldgraphics[Create Field Graphics] setup --> initsources[Init Sources] setup --> setupui[Setup UI] setupui --> panel-setup[Panel Selection] panel-setup --> frequency-row panel-setup --> button-row[Play/Pause Button Row] button-row --> toggleplaypause[Toggle Play/Pause] click createfieldgraphics href "#fn-createfieldgraphics" click initsources href "#fn-initsources" click setupui href "#fn-setupui" click panel-setup href "#sub-panel-setup" click button-row href "#sub-button-row" click toggleplaypause href "#fn-toggleplaypause" mousepressed --> canvas-check[Canvas Boundary Check] canvas-check --> distance-check[Source Hit Detection] distance-check --> source1-drag[Source 1 Dragging] distance-check --> source2-drag[Source 2 Dragging] source1-drag --> mousepressed source2-drag --> mousepressed click mousepressed href "#fn-mousepressed" click canvas-check href "#sub-canvas-check" click distance-check href "#sub-distance-check" click source1-drag href "#sub-source1-drag" click source2-drag href "#sub-source2-drag" mousedragged --> source1-drag mousedragged --> source2-drag click mousedragged href "#fn-mousedragged" mousereleased --> source1-drag mousereleased --> source2-drag click mousereleased href "#fn-mousereleased" windowresized --> createfieldgraphics click windowresized href "#fn-windowresized"

๐Ÿ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes all the data structures needed for the wave simulation, including the graphics buffer, source positions, and UI controls.

function setup() {
  createCanvas(windowWidth, windowHeight);
  pixelDensity(1); // make per-pixel math predictable

  createFieldGraphics();
  initSources();
  setupUI();

  timeSec = 0;
  lastMillis = millis();
}

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window
pixelDensity(1)
Sets pixel density to 1 for predictable pixel-by-pixel calculations (important for interference math)
createFieldGraphics()
Initializes the off-screen graphics buffer where the wave field is computed
initSources()
Sets up the two wave source positions and their properties
setupUI()
Creates the frequency slider and play/pause button in the control panel
timeSec = 0
Initializes the simulation time to zero
lastMillis = millis()
Records the current time in milliseconds to track elapsed time for animation

draw()

draw() runs continuously at 60 fps. Each frame, it updates the time, reads the frequency slider, computes the interference pattern, and displays everything on screen.

function draw() {
  updateTime();

  // Update frequency from slider
  if (frequencySlider) {
    frequency = parseFloat(frequencySlider.value());
    if (frequencyLabelSpan) {
      frequencyLabelSpan.html(nf(frequency, 1, 2) + ' Hz');
    }
  }

  background(0);

  renderField();          // compute interference pattern
  image(fieldG, 0, 0, width, height); // scale buffer onto canvas

  drawSources();
}

๐Ÿ”ง Subcomponents:

conditional Frequency Slider Update if (frequencySlider) { frequency = parseFloat(frequencySlider.value()); ... }

Reads the current slider value and updates the frequency variable and display label

calculation Render Pipeline renderField(); image(fieldG, 0, 0, width, height); drawSources();

Computes the wave interference pattern, displays it, and draws the source circles

Line by Line:

updateTime()
Updates the simulation time based on elapsed milliseconds since last frame
if (frequencySlider)
Checks if the frequency slider exists before trying to read its value
frequency = parseFloat(frequencySlider.value())
Gets the current slider value and converts it from text to a number
frequencyLabelSpan.html(nf(frequency, 1, 2) + ' Hz')
Updates the displayed frequency text to show the current value with 2 decimal places
background(0)
Clears the canvas with black color each frame
renderField()
Computes the wave interference pattern for all pixels in the field buffer
image(fieldG, 0, 0, width, height)
Displays the computed field buffer on the canvas, scaling it to fill the entire window
drawSources()
Draws the two source circles and their labels on top of the interference pattern

createFieldGraphics()

This function creates an off-screen graphics buffer at half resolution (fieldScale = 2). Computing waves at lower resolution improves performance while the image() function scales it back up to fill the canvas. This is a common optimization technique in graphics programming.

function createFieldGraphics() {
  const gWidth = max(1, floor(width / fieldScale));
  const gHeight = max(1, floor(height / fieldScale));
  fieldG = createGraphics(gWidth, gHeight);
  fieldG.pixelDensity(1);
}

๐Ÿ”ง Subcomponents:

calculation Scaled Dimensions const gWidth = max(1, floor(width / fieldScale)); const gHeight = max(1, floor(height / fieldScale));

Calculates lower-resolution dimensions for the field buffer to improve performance

Line by Line:

const gWidth = max(1, floor(width / fieldScale))
Divides canvas width by fieldScale (2) and rounds down, ensuring minimum width of 1 pixel
const gHeight = max(1, floor(height / fieldScale))
Divides canvas height by fieldScale (2) and rounds down, ensuring minimum height of 1 pixel
fieldG = createGraphics(gWidth, gHeight)
Creates an off-screen graphics buffer at the reduced resolution for wave computation
fieldG.pixelDensity(1)
Sets pixel density to 1 for the buffer to match canvas settings and ensure predictable pixel math

initSources()

This function creates two source objects with position and interaction properties. Each source is an object with x/y coordinates and properties to track if it's being dragged and by how much. The sources start symmetrically positioned on the canvas.

function initSources() {
  const midY = height * 0.5;
  const spacing = width * 0.18;
  const centerX = width * 0.5;

  source1 = {
    x: centerX - spacing,
    y: midY,
    isDragging: false,
    offsetX: 0,
    offsetY: 0
  };

  source2 = {
    x: centerX + spacing,
    y: midY,
    isDragging: false,
    offsetX: 0,
    offsetY: 0
  };
}

๐Ÿ”ง Subcomponents:

calculation Source Position Calculation const midY = height * 0.5; const spacing = width * 0.18; const centerX = width * 0.5;

Calculates initial positions for the two sources symmetrically placed horizontally

Line by Line:

const midY = height * 0.5
Calculates the vertical center of the canvas (50% down)
const spacing = width * 0.18
Calculates horizontal spacing between sources as 18% of canvas width
const centerX = width * 0.5
Calculates the horizontal center of the canvas
source1 = { x: centerX - spacing, y: midY, ... }
Creates source1 object positioned to the left of center with dragging properties
source2 = { x: centerX + spacing, y: midY, ... }
Creates source2 object positioned to the right of center with dragging properties
isDragging: false, offsetX: 0, offsetY: 0
Initializes dragging state and offset values used to track mouse interaction with each source

setupUI()

This function builds the user interface by creating HTML elements dynamically with p5.js functions. It creates a frequency slider with a label showing the current value, and a play/pause button. All elements are styled with CSS classes defined in style.css.

function setupUI() {
  const panel = select('#controls');
  if (!panel) return;

  // Frequency row
  const freqRow = createDiv();
  freqRow.parent(panel);
  freqRow.addClass('control-row');

  const freqLabel = createSpan('Frequency');
  freqLabel.parent(freqRow);
  freqLabel.addClass('label');

  // createSlider: https://p5js.org/reference/#/p5/createSlider
  frequencySlider = createSlider(minFrequency, maxFrequency, defaultFrequency, 0.01);
  frequencySlider.parent(freqRow);

  frequencyLabelSpan = createSpan(nf(defaultFrequency, 1, 2) + ' Hz');
  frequencyLabelSpan.parent(freqRow);

  // Play / Pause button
  const buttonRow = createDiv();
  buttonRow.parent(panel);
  buttonRow.addClass('control-row');

  // createButton: https://p5js.org/reference/#/p5/createButton
  playPauseButton = createButton('Pause');
  playPauseButton.parent(buttonRow);
  playPauseButton.mousePressed(togglePlayPause);
}

๐Ÿ”ง Subcomponents:

conditional Panel Selection const panel = select('#controls'); if (!panel) return;

Selects the HTML control panel and exits if it doesn't exist

calculation Frequency Control Row const freqRow = createDiv(); ... frequencySlider = createSlider(...)

Creates the frequency slider UI with label and value display

calculation Play/Pause Button Row const buttonRow = createDiv(); ... playPauseButton = createButton(...)

Creates the play/pause button and connects it to the toggle function

Line by Line:

const panel = select('#controls')
Selects the HTML element with id 'controls' where UI elements will be added
if (!panel) return
Exits the function early if the control panel doesn't exist in the HTML
const freqRow = createDiv()
Creates a new div container for the frequency control row
freqRow.parent(panel)
Adds the frequency row div as a child of the control panel
freqRow.addClass('control-row')
Applies CSS styling class to the frequency row
const freqLabel = createSpan('Frequency')
Creates a text label that says 'Frequency'
frequencySlider = createSlider(minFrequency, maxFrequency, defaultFrequency, 0.01)
Creates a slider with minimum 0.2 Hz, maximum 3.0 Hz, default 1.0 Hz, and 0.01 step size
frequencyLabelSpan = createSpan(nf(defaultFrequency, 1, 2) + ' Hz')
Creates a span to display the current frequency value formatted to 2 decimal places
playPauseButton = createButton('Pause')
Creates a button labeled 'Pause' that starts in pause mode
playPauseButton.mousePressed(togglePlayPause)
Connects the button to the togglePlayPause function so clicking it pauses/resumes the simulation

updateTime()

This function implements pause/play functionality by tracking elapsed time. When isPlaying is true, it accumulates time in timeSec. When paused, time stops advancing. This time value is used in renderField() to calculate wave phases.

function updateTime() {
  const now = millis();
  if (isPlaying) {
    const dt = (now - lastMillis) / 1000.0;
    timeSec += dt;
  }
  lastMillis = now;
}

๐Ÿ”ง Subcomponents:

conditional Time Delta Calculation if (isPlaying) { const dt = (now - lastMillis) / 1000.0; timeSec += dt; }

Only advances simulation time when playing, calculating elapsed time in seconds

Line by Line:

const now = millis()
Gets the current time in milliseconds since the sketch started
if (isPlaying)
Only updates time if the simulation is in playing state
const dt = (now - lastMillis) / 1000.0
Calculates the time elapsed since the last frame in seconds by subtracting previous time and dividing by 1000
timeSec += dt
Adds the elapsed time to the total simulation time, advancing the wave animation
lastMillis = now
Updates lastMillis to the current time for the next frame's calculation

togglePlayPause()

This callback function is triggered when the play/pause button is clicked. It toggles the simulation state and updates the button label to reflect the current state.

function togglePlayPause() {
  isPlaying = !isPlaying;
  if (playPauseButton) {
    playPauseButton.html(isPlaying ? 'Pause' : 'Play');
  }
}

๐Ÿ”ง Subcomponents:

conditional Play State Toggle isPlaying = !isPlaying; if (playPauseButton) { playPauseButton.html(isPlaying ? 'Pause' : 'Play'); }

Toggles the playing state and updates button text accordingly

Line by Line:

isPlaying = !isPlaying
Flips the isPlaying boolean (true becomes false, false becomes true)
if (playPauseButton)
Checks if the button exists before trying to update it
playPauseButton.html(isPlaying ? 'Pause' : 'Play')
Changes button text to 'Pause' if playing, or 'Play' if paused using a ternary operator

renderField()

This is the core physics function. It implements the wave interference equation by computing the phase of each wave at every pixel, then adding the amplitudes together. Where waves align (constructive interference), brightness is high. Where they cancel (destructive interference), brightness is low. The pixel loop iterates through the low-resolution buffer, which is then scaled up for display.

function renderField() {
  if (!fieldG) return;

  const g = fieldG;
  g.loadPixels(); // https://p5js.org/reference/#/p5.Image/loadPixels

  const gw = g.width;
  const gh = g.height;

  const simW = width;
  const simH = height;

  const omega = TWO_PI * frequency;      // angular frequency
  const k = TWO_PI / wavelength;         // wave number
  const maxAmp = 2.0;                    // max |A| for two unit waves

  for (let y = 0; y < gh; y++) {
    const vy = ((y + 0.5) / gh) * simH;

    for (let x = 0; x < gw; x++) {
      const vx = ((x + 0.5) / gw) * simW;

      // Distance to source 1
      const dx1 = vx - source1.x;
      const dy1 = vy - source1.y;
      const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);

      // Distance to source 2
      const dx2 = vx - source2.x;
      const dy2 = vy - source2.y;
      const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);

      // Phase = k * distance - omega * t
      const phase1 = k * d1 - omega * timeSec;
      const phase2 = k * d2 - omega * timeSec;

      // Wave amplitudes from each source
      const a1 = Math.sin(phase1);
      const a2 = Math.sin(phase2);

      // Combined amplitude (interference)
      const A = a1 + a2;
      const ampAbs = Math.abs(A);

      // Map |A| from 0..maxAmp to brightness 0..1
      const b = constrain(ampAbs / maxAmp, 0, 1);
      const blue = b * 255;

      const idx = 4 * (y * gw + x);
      g.pixels[idx + 0] = 0;        // R
      g.pixels[idx + 1] = 0;        // G
      g.pixels[idx + 2] = blue;     // B
      g.pixels[idx + 3] = 255;      // A
    }
  }

  g.updatePixels(); // https://p5js.org/reference/#/p5.Image/updatePixels
}

๐Ÿ”ง Subcomponents:

calculation Wave Physics Constants const omega = TWO_PI * frequency; const k = TWO_PI / wavelength; const maxAmp = 2.0;

Calculates angular frequency, wave number, and amplitude scaling for wave equations

for-loop Nested Pixel Loop for (let y = 0; y < gh; y++) { ... for (let x = 0; x < gw; x++) { ... } }

Iterates through every pixel in the field buffer to compute wave values

calculation Distance Calculations const dx1 = vx - source1.x; ... const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);

Calculates Euclidean distance from current pixel to each wave source

calculation Phase Calculation const phase1 = k * d1 - omega * timeSec; const phase2 = k * d2 - omega * timeSec;

Computes wave phase at current pixel for each source using wave equation

calculation Wave Interference const a1 = Math.sin(phase1); const a2 = Math.sin(phase2); const A = a1 + a2;

Calculates individual wave amplitudes and combines them to show interference

calculation Pixel Color Assignment const idx = 4 * (y * gw + x); g.pixels[idx + 0] = 0; g.pixels[idx + 1] = 0; g.pixels[idx + 2] = blue; g.pixels[idx + 3] = 255;

Sets the RGBA color values for each pixel based on interference amplitude

Line by Line:

if (!fieldG) return
Exits early if the field graphics buffer hasn't been created
const g = fieldG
Creates a shorthand reference to the field graphics buffer
g.loadPixels()
Loads the pixel data from the graphics buffer into memory for direct manipulation
const gw = g.width
Gets the width of the field buffer (half canvas width due to fieldScale)
const gh = g.height
Gets the height of the field buffer (half canvas height due to fieldScale)
const omega = TWO_PI * frequency
Calculates angular frequency (radians per second) from the frequency in Hz
const k = TWO_PI / wavelength
Calculates wave number (spatial frequency) from the fixed wavelength
const maxAmp = 2.0
Sets maximum amplitude to 2.0 (sum of two unit sine waves at constructive interference)
const vy = ((y + 0.5) / gh) * simH
Maps pixel y-coordinate from buffer space to canvas space, using +0.5 for pixel center
const vx = ((x + 0.5) / gw) * simW
Maps pixel x-coordinate from buffer space to canvas space, using +0.5 for pixel center
const d1 = Math.sqrt(dx1 * dx1 + dy1 * dy1)
Calculates Euclidean distance from current pixel to source1
const d2 = Math.sqrt(dx2 * dx2 + dy2 * dy2)
Calculates Euclidean distance from current pixel to source2
const phase1 = k * d1 - omega * timeSec
Calculates wave phase at this pixel from source1 using standard wave equation
const phase2 = k * d2 - omega * timeSec
Calculates wave phase at this pixel from source2 using standard wave equation
const a1 = Math.sin(phase1)
Calculates amplitude contribution from source1 using sine of the phase
const a2 = Math.sin(phase2)
Calculates amplitude contribution from source2 using sine of the phase
const A = a1 + a2
Adds the two amplitudes together - this is where constructive/destructive interference occurs
const ampAbs = Math.abs(A)
Takes absolute value of combined amplitude to get brightness (always positive)
const b = constrain(ampAbs / maxAmp, 0, 1)
Normalizes amplitude to 0-1 range by dividing by maxAmp and clamping to valid range
const blue = b * 255
Converts normalized brightness (0-1) to blue channel value (0-255)
const idx = 4 * (y * gw + x)
Calculates the pixel array index (each pixel has 4 values: R, G, B, A)
g.pixels[idx + 0] = 0
Sets red channel to 0 (no red in the output)
g.pixels[idx + 1] = 0
Sets green channel to 0 (no green in the output)
g.pixels[idx + 2] = blue
Sets blue channel to the computed brightness value
g.pixels[idx + 3] = 255
Sets alpha (transparency) to 255 (fully opaque)
g.updatePixels()
Applies all the pixel changes to the graphics buffer so they become visible

drawSources()

This function draws the visual representation of the two wave sources on top of the interference pattern. Each source is shown as a blue circle with a white letter label (A or B). This function is called every frame after the interference pattern is rendered.

function drawSources() {
  stroke(255);
  strokeWeight(2);
  fill(80, 160, 255, 200);

  const d = sourceRadius * 2;
  ellipse(source1.x, source1.y, d, d);
  ellipse(source2.x, source2.y, d, d);

  noStroke();
  fill(255);
  textAlign(CENTER, CENTER);
  textSize(12);
  text('A', source1.x, source1.y);
  text('B', source2.x, source2.y);
}

๐Ÿ”ง Subcomponents:

calculation Source Circle Drawing const d = sourceRadius * 2; ellipse(source1.x, source1.y, d, d); ellipse(source2.x, source2.y, d, d);

Draws two blue circles at the source positions

calculation Source Label Drawing text('A', source1.x, source1.y); text('B', source2.x, source2.y);

Draws letter labels A and B at the center of each source

Line by Line:

stroke(255)
Sets the outline color to white
strokeWeight(2)
Sets the outline thickness to 2 pixels
fill(80, 160, 255, 200)
Sets the fill color to light blue with 200/255 opacity (semi-transparent)
const d = sourceRadius * 2
Calculates the diameter of the source circles (radius is 14, so diameter is 28)
ellipse(source1.x, source1.y, d, d)
Draws a circle at source1's position with calculated diameter
ellipse(source2.x, source2.y, d, d)
Draws a circle at source2's position with calculated diameter
noStroke()
Turns off outline drawing for the text labels
fill(255)
Sets text color to white
textAlign(CENTER, CENTER)
Aligns text to be centered both horizontally and vertically
textSize(12)
Sets font size to 12 pixels
text('A', source1.x, source1.y)
Draws the letter 'A' centered at source1's position
text('B', source2.x, source2.y)
Draws the letter 'B' centered at source2's position

mousePressed()

This function handles the initial mouse click. It checks if the click hit either source and starts the dragging process by storing the offset between the mouse and the source center. This offset is used in mouseDragged() to maintain smooth dragging.

function mousePressed() {
  if (!insideCanvas(mouseX, mouseY)) return;

  const d1 = dist(mouseX, mouseY, source1.x, source1.y);
  const d2 = dist(mouseX, mouseY, source2.x, source2.y);

  if (d1 <= sourceRadius + 6) {
    source1.isDragging = true;
    source1.offsetX = mouseX - source1.x;
    source1.offsetY = mouseY - source1.y;
  } else if (d2 <= sourceRadius + 6) {
    source2.isDragging = true;
    source2.offsetX = mouseX - source2.x;
    source2.offsetY = mouseY - source2.y;
  }
}

๐Ÿ”ง Subcomponents:

conditional Canvas Boundary Check if (!insideCanvas(mouseX, mouseY)) return;

Ignores clicks outside the canvas area

conditional Source Hit Detection if (d1 <= sourceRadius + 6) { ... } else if (d2 <= sourceRadius + 6) { ... }

Determines which source (if any) was clicked based on distance from mouse to source

Line by Line:

if (!insideCanvas(mouseX, mouseY)) return
Exits the function if the mouse click was outside the canvas boundaries
const d1 = dist(mouseX, mouseY, source1.x, source1.y)
Calculates the distance from the mouse click to source1
const d2 = dist(mouseX, mouseY, source2.x, source2.y)
Calculates the distance from the mouse click to source2
if (d1 <= sourceRadius + 6)
Checks if click was within sourceRadius + 6 pixels of source1 (hit detection with padding)
source1.isDragging = true
Marks source1 as being dragged
source1.offsetX = mouseX - source1.x
Stores the offset between mouse position and source1's center for smooth dragging
source1.offsetY = mouseY - source1.y
Stores the vertical offset between mouse position and source1's center
else if (d2 <= sourceRadius + 6)
Checks if click was within sourceRadius + 6 pixels of source2
source2.isDragging = true
Marks source2 as being dragged
source2.offsetX = mouseX - source2.x
Stores the offset between mouse position and source2's center
source2.offsetY = mouseY - source2.y
Stores the vertical offset between mouse position and source2's center

mouseDragged()

This function is called continuously while the mouse is being dragged. It updates the position of whichever source is being dragged, using the offset calculated in mousePressed() to keep the source centered under the cursor. The constrain() function prevents sources from being dragged outside the canvas.

function mouseDragged() {
  if (!insideCanvas(mouseX, mouseY)) return;

  if (source1.isDragging) {
    source1.x = constrain(mouseX - source1.offsetX, 0, width);
    source1.y = constrain(mouseY - source1.offsetY, 0, height);
  } else if (source2.isDragging) {
    source2.x = constrain(mouseX - source2.offsetX, 0, width);
    source2.y = constrain(mouseY - source2.offsetY, 0, height);
  }
}

๐Ÿ”ง Subcomponents:

conditional Source 1 Dragging if (source1.isDragging) { source1.x = constrain(mouseX - source1.offsetX, 0, width); source1.y = constrain(mouseY - source1.offsetY, 0, height); }

Updates source1 position while keeping it within canvas boundaries

conditional Source 2 Dragging else if (source2.isDragging) { source2.x = constrain(mouseX - source2.offsetX, 0, width); source2.y = constrain(mouseY - source2.offsetY, 0, height); }

Updates source2 position while keeping it within canvas boundaries

Line by Line:

if (!insideCanvas(mouseX, mouseY)) return
Stops dragging if the mouse moves outside the canvas
if (source1.isDragging)
Checks if source1 is currently being dragged
source1.x = constrain(mouseX - source1.offsetX, 0, width)
Updates source1's x position using the stored offset, keeping it between 0 and canvas width
source1.y = constrain(mouseY - source1.offsetY, 0, height)
Updates source1's y position using the stored offset, keeping it between 0 and canvas height
else if (source2.isDragging)
Checks if source2 is currently being dragged
source2.x = constrain(mouseX - source2.offsetX, 0, width)
Updates source2's x position using the stored offset, keeping it within bounds
source2.y = constrain(mouseY - source2.offsetY, 0, height)
Updates source2's y position using the stored offset, keeping it within bounds

mouseReleased()

This function is called when the mouse button is released. It ends the dragging state for both sources, preventing further position updates until the user clicks and drags again.

function mouseReleased() {
  source1.isDragging = false;
  source2.isDragging = false;
}

Line by Line:

source1.isDragging = false
Stops dragging source1 when the mouse button is released
source2.isDragging = false
Stops dragging source2 when the mouse button is released

insideCanvas(x, y)

This is a simple utility function that checks if a point (x, y) is inside the canvas. It returns true only if x is between 0 and width AND y is between 0 and height. Used to prevent interactions outside the canvas area.

function insideCanvas(x, y) {
  return x >= 0 && x <= width && y >= 0 && y <= height;
}

Line by Line:

return x >= 0 && x <= width && y >= 0 && y <= height
Returns true if the coordinates are within the canvas bounds, false otherwise

windowResized()

This function is called automatically by p5.js whenever the browser window is resized. It updates the canvas size and recreates the field buffer to match the new dimensions, ensuring the simulation continues to work correctly at any window size.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  pixelDensity(1);
  createFieldGraphics();
}

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Resizes the p5.js canvas to match the new window dimensions
pixelDensity(1)
Resets pixel density to 1 after resizing
createFieldGraphics()
Recreates the field graphics buffer at the new canvas size

๐Ÿ“ฆ Key Variables

fieldG p5.Graphics

Off-screen graphics buffer where the wave interference pattern is computed at reduced resolution for performance

let fieldG;
fieldScale number

Resolution divisor - simulation runs at canvas size divided by this value (2 means half resolution)

const fieldScale = 2;
source1 object

First wave source with properties: x, y (position), isDragging (state), offsetX, offsetY (mouse offset for dragging)

let source1 = { x: 0, y: 0, isDragging: false, offsetX: 0, offsetY: 0 };
source2 object

Second wave source with same properties as source1

let source2 = { x: 0, y: 0, isDragging: false, offsetX: 0, offsetY: 0 };
sourceRadius number

Radius of the source circles in pixels (14 pixels), used for visual size and hit detection

const sourceRadius = 14;
frequencySlider p5.Renderer

HTML slider element that controls the wave frequency from 0.2 to 3.0 Hz

let frequencySlider;
frequencyLabelSpan p5.Renderer

HTML span element that displays the current frequency value with 2 decimal places

let frequencyLabelSpan;
playPauseButton p5.Renderer

HTML button element that toggles between play and pause states

let playPauseButton;
defaultFrequency number

Initial frequency value in Hz (1.0 Hz)

const defaultFrequency = 1.0;
minFrequency number

Minimum frequency value allowed by the slider (0.2 Hz)

const minFrequency = 0.2;
maxFrequency number

Maximum frequency value allowed by the slider (3.0 Hz)

const maxFrequency = 3.0;
wavelength number

Fixed wavelength in pixels (150 pixels) - determines spatial scale of interference pattern

const wavelength = 150;
frequency number

Current wave frequency in Hz, controlled by the slider and used in wave calculations

let frequency = defaultFrequency;
timeSec number

Elapsed simulation time in seconds, used to calculate wave phases and create animation

let timeSec = 0;
lastMillis number

Timestamp in milliseconds from the previous frame, used to calculate delta time

let lastMillis = 0;
isPlaying boolean

Flag indicating whether the simulation is playing (true) or paused (false)

let isPlaying = true;

๐Ÿงช Try This!

Experiment with the code by making these changes:

  1. Change the wavelength constant from 150 to 50 to see much tighter interference patterns with more bands. Smaller wavelength = more oscillations across the canvas.
  2. Modify the maxFrequency from 3.0 to 10.0 to allow much faster wave oscillations. Drag the slider all the way right to see rapid pulsing patterns.
  3. Change the color output in renderField() from blue (g.pixels[idx + 2] = blue) to red by instead setting g.pixels[idx + 0] = blue. This creates a red interference pattern.
  4. Drag the two sources very close together and observe how the interference pattern changes - when sources are close, the wavelength becomes more visible.
  5. Drag the two sources far apart (near opposite edges) and watch how the interference pattern becomes more complex with many more bands.
  6. Add a third wave source by creating source3 and calculating its distance/phase in renderField(). This creates three-way interference patterns.
  7. Change fieldScale from 2 to 1 for full-resolution computation (slower but more detailed), or increase it to 4 for faster performance with less detail.
  8. Modify the fill color in drawSources() from fill(80, 160, 255, 200) to fill(255, 0, 0, 200) to make the sources red instead of blue.
Open in Editor & Experiment โ†’

๐Ÿ”ง Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE renderField() - nested loop

The nested loop recalculates distance and phase for every pixel every frame, even when sources haven't moved. This is computationally expensive.

๐Ÿ’ก Cache source positions and only recalculate the field when sources move or frequency changes. Use a dirty flag to skip rendering when nothing has changed.

BUG mouseDragged()

If the user drags a source outside the canvas and releases, then tries to drag it again, the offset calculation will be incorrect because the source position was constrained but the offset wasn't updated.

๐Ÿ’ก Update the offset values in mouseDragged() when constrain() changes the position: source1.offsetX = mouseX - source1.x after updating source1.x.

FEATURE setupUI()

There's no wavelength control - users can only adjust frequency. The wavelength is hardcoded at 150 pixels, limiting exploration of how wavelength affects interference.

๐Ÿ’ก Add another slider for wavelength (e.g., 50-300 pixels) and update the wavelength variable in draw() similar to how frequency is updated.

STYLE renderField()

Magic numbers like maxAmp = 2.0, wavelength = 150, and sourceRadius = 14 are scattered throughout the code without clear explanation of their physical meaning.

๐Ÿ’ก Add comments explaining the physics: maxAmp = 2.0 because two unit sine waves can sum to ยฑ2, wavelength in pixels determines spatial frequency, sourceRadius is arbitrary visual size.

BUG setupUI()

If the HTML element with id 'controls' doesn't exist, setupUI() returns silently without creating any UI, but the sketch still tries to use frequencySlider and playPauseButton, causing null reference errors.

๐Ÿ’ก Either ensure the HTML element exists, or create a fallback UI panel dynamically if it's missing: if (!panel) { panel = createDiv(); panel.id('controls'); }

PERFORMANCE renderField() - pixel array access

Accessing g.pixels[idx + n] repeatedly is slower than using typed arrays. Modern p5.js uses Uint8ClampedArray which could be optimized.

๐Ÿ’ก Consider using g.pixels.set() for batch updates, or pre-compute color values in a temporary array before writing to pixels for better cache locality.

Preview

AI Wave Function Simulator - Physics Interference Visualization Explore wave interference physics! - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Wave Function Simulator - Physics Interference Visualization Explore wave interference physics! - Code flow showing setup, draw, createfieldgraphics, initsources, setupui, updatetime, toggleplaypause, renderfield, drawsources, mousepressed, mousedragged, mousereleased, insidecanvas, windowresized
Code Flow Diagram