AI Fractal Explorer - Infinite Mandelbrot Zoom Explore infinite mathematical beauty! Click to zoom

This sketch creates an interactive Mandelbrot fractal explorer that renders the infinite mathematical beauty of the Mandelbrot set pixel-by-pixel. Users can click to zoom in 2x at the cursor, drag to pan across the complex plane, and scroll to zoom in/out smoothly. The fractal features animated psychedelic colors that cycle continuously, with iteration depth that increases as you zoom deeper.

πŸŽ“ Concepts You'll Learn

Mandelbrot set algorithmComplex number arithmeticEscape-time coloringSmooth normalizationColor animationInteractive zoom and panHSV to RGB conversionPixel manipulationPerformance optimization with Float32ArrayMathematical visualization

πŸ”„ Code Flow

Code flow showing setup, resizebuffers, computemaxiterations, computefractal, palettecolor, hsvtorgb, recolorfractal, draw, drawhud, screentocomplex, mousepressed, mousedragged, mousereleased, mousewheel, windowresized, log10

πŸ’‘ Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> resizebuffers[resizebuffers] setup --> draw[draw loop] draw --> dirtycheck[dirty-check] dirtycheck -->|true| computemaxiterations[computemaxiterations] computemaxiterations --> logzoom[log-zoom] logzoom --> itercalc[iter-calc] itercalc --> computefractal[computefractal] computefractal --> pixelloop1[pixel-loop] pixelloop1 --> complexmapping[complex-mapping] complexmapping --> mandelbrotiteration[mandelbrot-iteration] mandelbrotiteration --> interiorcheck[interior-check] interiorcheck -->|inside| toneemphasis[tone-emphasis] interiorcheck -->|outside| smoothcoloring[smooth-coloring] smoothcoloring --> hueanimation[hue-animation] hueanimation --> hsvsector[hsv-sector] hsvsector --> componentcalc[component-calc] componentcalc --> sectorSwitch[sector-switch] sectorSwitch --> pixelloop2[pixel-loop] pixelloop2 --> recolorfractal[recolorfractal] recolorfractal --> paletteanimation[palette-animation] paletteanimation --> drawhud[drawhud] drawhud --> draw draw -->|60 times/sec| draw click setup href "#fn-setup" click resizebuffers href "#fn-resizebuffers" click draw href "#fn-draw" click dirtycheck href "#sub-dirty-check" click computemaxiterations href "#fn-computemaxiterations" click logzoom href "#sub-log-zoom" click itercalc href "#sub-iter-calc" click computefractal href "#fn-computefractal" click pixelloop1 href "#sub-pixel-loop" click complexmapping href "#sub-complex-mapping" click mandelbrotiteration href "#sub-mandelbrot-iteration" click interiorcheck href "#sub-interior-check" click toneemphasis href "#sub-tone-emphasis" click smoothcoloring href "#sub-smooth-coloring" click hueanimation href "#sub-hue-animation" click hsvsector href "#sub-hsv-sector" click componentcalc href "#sub-component-calc" click sectorSwitch href "#sub-sector-switch" click pixelloop2 href "#sub-pixel-loop" click recolorfractal href "#fn-recolorfractal" click paletteanimation href "#sub-palette-animation" click drawhud href "#fn-drawhud"

πŸ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, prepares data structures, and renders the first frame of the Mandelbrot set.

function setup() {
  createCanvas(windowWidth, windowHeight);
  pixelDensity(1); // Important for consistent pixel math
  resizeBuffers();
  computeFractal(); // Initial render
}

Line by Line:

createCanvas(windowWidth, windowHeight)
Creates a canvas that fills the entire browser window
pixelDensity(1)
Sets pixel density to 1 for consistent pixel-to-coordinate mapping (important for Mandelbrot math)
resizeBuffers()
Initializes the fractal image and iteration data array based on canvas size
computeFractal()
Calculates the initial Mandelbrot set rendering for the default view

resizeBuffers()

This function allocates memory for storing fractal data. Float32Array is more efficient than regular arrays for large datasets. The viewDirty flag prevents unnecessary recalculation.

function resizeBuffers() {
  fractalImg = createImage(width, height);
  iterData = new Float32Array(width * height);
  viewDirty = true;
}

Line by Line:

fractalImg = createImage(width, height)
Creates a new p5.Image object that will store the colored fractal pixels
iterData = new Float32Array(width * height)
Creates a typed array to store normalized escape-time values (0..1) for each pixel, using Float32 for memory efficiency
viewDirty = true
Sets a flag indicating the fractal needs to be recomputed because buffers changed

computeMaxIterations()

This function adapts iteration depth to zoom level. Deeper zooms need more iterations to reveal fine detail, but we cap it at 600 to prevent slowdown. The logarithmic scaling ensures smooth transitions.

function computeMaxIterations() {
  const zoom = BASE_VIEW_WIDTH / viewWidth;
  const logZoom = max(0, log10(zoom + 1));
  let maxIter = 60 + Math.floor(logZoom * 80);
  maxIter = constrain(maxIter, 60, 600);
  return maxIter;
}

πŸ”§ Subcomponents:

calculation Zoom Level Calculation const zoom = BASE_VIEW_WIDTH / viewWidth

Calculates how much we've zoomed in by comparing current view width to base width

calculation Logarithmic Zoom Scaling const logZoom = max(0, log10(zoom + 1))

Converts zoom level to logarithmic scale so iteration increases smoothly

calculation Iteration Depth Scaling let maxIter = 60 + Math.floor(logZoom * 80)

Increases iteration count as zoom increases, with base of 60 iterations

Line by Line:

const zoom = BASE_VIEW_WIDTH / viewWidth
Calculates zoom factor: if viewWidth is half of base, zoom = 2x
const logZoom = max(0, log10(zoom + 1))
Converts zoom to log scale (base 10) so we can scale iterations smoothly; +1 prevents log(0)
let maxIter = 60 + Math.floor(logZoom * 80)
Sets iteration count: starts at 60, adds up to 80 per order of magnitude of zoom
maxIter = constrain(maxIter, 60, 600)
Clamps iteration count between 60 (minimum) and 600 (maximum) to balance quality and performance
return maxIter
Returns the calculated iteration depth for use in Mandelbrot computation

computeFractal()

This is the core Mandelbrot algorithm. For each pixel, we map it to a complex number c, then iterate z = zΒ² + c until it escapes (magnitude > 2) or reaches max iterations. The smooth coloring algorithm uses logarithms to create continuous color gradients instead of banding.

function computeFractal() {
  const w = width;
  const h = height;
  const scale = viewWidth / w; // same for x and y to preserve aspect
  const maxIter = computeMaxIterations();
  const data = iterData;
  const escapeRadiusSquared = 4;

  for (let py = 0; py < h; py++) {
    const cy = centerY + (py - h / 2) * scale;

    for (let px = 0; px < w; px++) {
      const cx = centerX + (px - w / 2) * scale;

      let zx = 0;
      let zy = 0;
      let zx2 = 0;
      let zy2 = 0;
      let iter = 0;

      // Mandelbrot iteration: z_{n+1} = z_n^2 + c
      while (iter < maxIter && zx2 + zy2 <= escapeRadiusSquared) {
        zy = 2 * zx * zy + cy;
        zx = zx2 - zy2 + cx;
        zx2 = zx * zx;
        zy2 = zy * zy;
        iter++;
      }

      const index = py * w + px;

      if (iter === maxIter) {
        // Likely inside the set
        data[index] = -1;
      } else {
        // Smooth coloring (normalized iteration count)
        // Reference: https://en.wikipedia.org/wiki/Mandelbrot_set#Continuous_(smooth)_coloring
        const log_zn = Math.log(zx2 + zy2) / 2;
        const nu = Math.log(log_zn / Math.log(2)) / Math.log(2);
        const smoothIter = iter + 1 - nu;
        let t = smoothIter / maxIter;
        if (!isFinite(t)) t = 0;
        data[index] = t;
      }
    }
  }

  viewDirty = false;
}

πŸ”§ Subcomponents:

for-loop Nested Pixel Loop for (let py = 0; py < h; py++) { for (let px = 0; px < w; px++) {

Iterates through every pixel on the canvas to compute its Mandelbrot value

calculation Screen to Complex Plane Mapping const cx = centerX + (px - w / 2) * scale; const cy = centerY + (py - h / 2) * scale

Converts pixel coordinates to complex plane coordinates (real and imaginary parts)

while-loop Mandelbrot Iteration while (iter < maxIter && zx2 + zy2 <= escapeRadiusSquared) { zy = 2 * zx * zy + cy; zx = zx2 - zy2 + cx; zx2 = zx * zx; zy2 = zy * zy; iter++; }

Repeatedly applies z = zΒ² + c until escape or max iterations reached

conditional Smooth Color Normalization const log_zn = Math.log(zx2 + zy2) / 2; const nu = Math.log(log_zn / Math.log(2)) / Math.log(2); const smoothIter = iter + 1 - nu; let t = smoothIter / maxIter

Applies smooth coloring algorithm to eliminate banding and create continuous color gradients

Line by Line:

const w = width; const h = height
Caches canvas width and height for faster access in loops
const scale = viewWidth / w
Calculates how many complex-plane units each pixel represents
const maxIter = computeMaxIterations()
Gets the iteration depth based on current zoom level
for (let py = 0; py < h; py++) { const cy = centerY + (py - h / 2) * scale
Outer loop iterates through each row; cy maps pixel row to imaginary (vertical) coordinate
for (let px = 0; px < w; px++) { const cx = centerX + (px - w / 2) * scale
Inner loop iterates through each column; cx maps pixel column to real (horizontal) coordinate
let zx = 0; let zy = 0; let zx2 = 0; let zy2 = 0; let iter = 0
Initializes z to (0,0) and squared components; iter counts iterations
while (iter < maxIter && zx2 + zy2 <= escapeRadiusSquared)
Continues iterating while within max iterations AND z hasn't escaped (magnitude ≀ 2)
zy = 2 * zx * zy + cy
Complex multiplication: imaginary part of (zx + i*zy)Β² + (cx + i*cy)
zx = zx2 - zy2 + cx
Complex multiplication: real part of (zx + i*zy)Β² + (cx + i*cy)
zx2 = zx * zx; zy2 = zy * zy
Precalculates squared components for next iteration and escape check
if (iter === maxIter) { data[index] = -1
If we hit max iterations, point is likely inside the set; store -1 as sentinel value
const log_zn = Math.log(zx2 + zy2) / 2
Calculates log of final z magnitude for smooth coloring algorithm
const nu = Math.log(log_zn / Math.log(2)) / Math.log(2)
Computes fractional iteration count to eliminate color banding
const smoothIter = iter + 1 - nu
Combines integer iterations with fractional part for smooth color transition
let t = smoothIter / maxIter; if (!isFinite(t)) t = 0
Normalizes to 0..1 range; handles NaN/Infinity edge cases by defaulting to 0
data[index] = t
Stores normalized escape-time value in the data array for later coloring

paletteColor(t)

This function maps the normalized escape-time value to a psychedelic color. The paletteOffset animates continuously, creating a swirling color effect. Interior points are always black, while exterior points cycle through the full color spectrum based on escape speed.

function paletteColor(t) {
  if (t < 0) {
    // Interior of the set: black
    return { r: 0, g: 0, b: 0 };
  }

  t = constrain(t, 0, 1);
  t = Math.sqrt(t); // Emphasize mid-tones

  const hue = (paletteOffset + t * 0.7) % 1; // swirl through hues
  const sat = 1.0;
  const val = 1.0;

  return hsvToRgb(hue, sat, val);
}

πŸ”§ Subcomponents:

conditional Interior Detection if (t < 0) { return { r: 0, g: 0, b: 0 }; }

Returns black for points inside the Mandelbrot set

calculation Tone Emphasis t = Math.sqrt(t)

Applies square root to emphasize mid-tone colors and reduce darkness

calculation Animated Hue Shift const hue = (paletteOffset + t * 0.7) % 1

Creates swirling color animation by adding paletteOffset and scaling escape-time value

Line by Line:

if (t < 0) { return { r: 0, g: 0, b: 0 }; }
Negative t indicates interior points (stored as -1); these are always black
t = constrain(t, 0, 1)
Clamps t to valid 0..1 range in case of floating-point errors
t = Math.sqrt(t)
Square root transformation brightens darker colors, creating more vibrant mid-tones
const hue = (paletteOffset + t * 0.7) % 1
Creates hue by adding animated offset to scaled escape-time; % 1 wraps around 0..1
const sat = 1.0; const val = 1.0
Full saturation and value create vivid, bright colors
return hsvToRgb(hue, sat, val)
Converts HSV color to RGB object for pixel rendering

hsvToRgb(h, s, v)

This is the standard HSV to RGB conversion algorithm. HSV (Hue, Saturation, Value) is intuitive for color animation, but displays require RGB. The algorithm maps hue to one of 6 color sectors, then interpolates RGB values based on saturation and value.

function hsvToRgb(h, s, v) {
  let r, g, b;
  const i = Math.floor(h * 6);
  const f = h * 6 - i;
  const p = v * (1 - s);
  const q = v * (1 - f * s);
  const t = v * (1 - (1 - f) * s);

  switch (i % 6) {
    case 0:
      r = v; g = t; b = p; break;
    case 1:
      r = q; g = v; b = p; break;
    case 2:
      r = p; g = v; b = t; break;
    case 3:
      r = p; g = q; b = v; break;
    case 4:
      r = t; g = p; b = v; break;
    case 5:
      r = v; g = p; b = q; break;
  }

  return {
    r: Math.round(r * 255),
    g: Math.round(g * 255),
    b: Math.round(b * 255)
  };
}

πŸ”§ Subcomponents:

calculation Hue Sector Calculation const i = Math.floor(h * 6); const f = h * 6 - i

Divides hue wheel into 6 sectors and calculates fractional position within sector

calculation RGB Component Calculation const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s)

Precalculates intermediate RGB values used in different hue sectors

switch-case Sector-Based RGB Assignment switch (i % 6) { case 0: r = v; g = t; b = p; break; ... }

Assigns RGB values based on which hue sector we're in

Line by Line:

const i = Math.floor(h * 6)
Divides hue (0..1) into 6 sectors (0..5) representing the color wheel
const f = h * 6 - i
Calculates fractional position (0..1) within the current hue sector
const p = v * (1 - s)
Precalculates the 'p' component: value reduced by saturation
const q = v * (1 - f * s)
Precalculates the 'q' component: value reduced by fractional saturation
const t = v * (1 - (1 - f) * s)
Precalculates the 't' component: value reduced by inverse-fractional saturation
switch (i % 6) { ... }
Routes to different RGB assignments based on hue sector (0=red, 1=yellow, 2=green, 3=cyan, 4=blue, 5=magenta)
Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)
Converts RGB from 0..1 range to 0..255 range and rounds to integers for pixel data

recolorFractal()

This function separates computation from coloring. The Mandelbrot escape-times are computed once per view change, but colors are recomputed every frame for animation. This is efficient because the expensive Mandelbrot math only runs when needed.

function recolorFractal() {
  fractalImg.loadPixels();
  const pixels = fractalImg.pixels;
  const data = iterData;

  let pIndex = 0;
  const len = data.length;

  for (let i = 0; i < len; i++) {
    const t = data[i];
    const col = paletteColor(t);
    pixels[pIndex++] = col.r;
    pixels[pIndex++] = col.g;
    pixels[pIndex++] = col.b;
    pixels[pIndex++] = 255;
  }

  fractalImg.updatePixels();
}

πŸ”§ Subcomponents:

for-loop Pixel Color Assignment Loop for (let i = 0; i < len; i++) { const t = data[i]; const col = paletteColor(t); pixels[pIndex++] = col.r; pixels[pIndex++] = col.g; pixels[pIndex++] = col.b; pixels[pIndex++] = 255; }

Iterates through all escape-time values and converts them to RGBA pixel data

Line by Line:

fractalImg.loadPixels()
Prepares the image for pixel-level manipulation by loading its pixel array
const pixels = fractalImg.pixels
Gets reference to the pixel array (RGBA format: 4 values per pixel)
const data = iterData
Gets reference to the precomputed escape-time values
let pIndex = 0; const len = data.length
Initializes pixel array index and caches data array length for loop
for (let i = 0; i < len; i++) { const t = data[i]; const col = paletteColor(t)
Loops through each escape-time value and converts it to an RGB color
pixels[pIndex++] = col.r; pixels[pIndex++] = col.g; pixels[pIndex++] = col.b; pixels[pIndex++] = 255
Writes RGBA values to pixel array (R, G, B, then full alpha opacity)
fractalImg.updatePixels()
Applies all pixel changes to the image so they display on screen

draw()

draw() runs 60 times per second. It efficiently separates concerns: only recompute Mandelbrot when view changes, animate colors every frame, then display. This keeps the frame rate high even during complex calculations.

function draw() {
  // Recompute fractal if view changed
  if (viewDirty) {
    computeFractal();
  }

  // Animate color palette
  paletteOffset = (paletteOffset + PALETTE_SPEED) % 1;

  // Apply the current palette to the precomputed escape data
  recolorFractal();

  // Draw fractal
  background(0);
  image(fractalImg, 0, 0, width, height);

  drawHUD();
}

πŸ”§ Subcomponents:

conditional Dirty Flag Check if (viewDirty) { computeFractal(); }

Only recomputes expensive Mandelbrot math when view has changed

calculation Palette Animation paletteOffset = (paletteOffset + PALETTE_SPEED) % 1

Continuously cycles color palette offset for psychedelic animation effect

Line by Line:

if (viewDirty) { computeFractal(); }
Checks if view changed (zoom/pan); only recomputes expensive fractal if needed
paletteOffset = (paletteOffset + PALETTE_SPEED) % 1
Increments color animation offset by PALETTE_SPEED each frame, wrapping at 1.0
recolorFractal()
Applies current palette to precomputed escape-time data, creating animated color effect
background(0)
Clears canvas with black background
image(fractalImg, 0, 0, width, height)
Draws the colored fractal image to fill the entire canvas
drawHUD()
Renders the heads-up display with instructions and zoom level

drawHUD()

The HUD provides real-time feedback about zoom level and interaction instructions. The zoom display uses logarithmic scale (powers of 10) because Mandelbrot zoom is exponentialβ€”you can zoom infinitely deep.

function drawHUD() {
  push();
  noStroke();
  fill(0, 160);
  const boxWidth = 260;
  const boxHeight = 72;
  rect(10, 10, boxWidth, boxHeight, 4);

  fill(255);
  textSize(12);
  textFont('monospace');
  text('Mandelbrot Explorer', 18, 28);

  const zoom = BASE_VIEW_WIDTH / viewWidth;
  const logZoom = zoom > 0 ? log10(zoom) : 0;
  text(`Zoom: 10^${nf(logZoom, 1, 2)}`, 18, 44);
  text('Click: zoom in 2x at cursor', 18, 60);
  text('Drag to pan, scroll to zoom', 18, 76);
  pop();
}

πŸ”§ Subcomponents:

calculation HUD Background Box fill(0, 160); rect(10, 10, boxWidth, boxHeight, 4)

Draws semi-transparent black box for text background

calculation Zoom Level Display const zoom = BASE_VIEW_WIDTH / viewWidth; const logZoom = zoom > 0 ? log10(zoom) : 0; text(`Zoom: 10^${nf(logZoom, 1, 2)}`, 18, 44)

Calculates and displays current zoom level in scientific notation

Line by Line:

push()
Saves current graphics state (fill, stroke, font settings)
noStroke(); fill(0, 160)
Removes outline and sets fill to semi-transparent black (alpha 160)
rect(10, 10, boxWidth, boxHeight, 4)
Draws background box at top-left with 4-pixel rounded corners
fill(255); textSize(12); textFont('monospace')
Sets text to white, 12pt, monospace font for readability
const zoom = BASE_VIEW_WIDTH / viewWidth
Calculates zoom factor (how much we've zoomed in from initial view)
const logZoom = zoom > 0 ? log10(zoom) : 0
Converts zoom to log scale for scientific notation display
text(`Zoom: 10^${nf(logZoom, 1, 2)}`, 18, 44)
Displays zoom as power of 10 (e.g., 10^2.5 = 316x zoom)
pop()
Restores previous graphics state

screenToComplex(sx, sy)

This helper function is crucial for interaction. It converts screen pixel coordinates to complex plane coordinates, allowing clicks and mouse movements to map correctly to the mathematical space we're exploring.

function screenToComplex(sx, sy) {
  const scale = viewWidth / width;
  const re = centerX + (sx - width / 2) * scale;
  const im = centerY + (sy - height / 2) * scale;
  return { re, im };
}

πŸ”§ Subcomponents:

calculation Screen to Complex Mapping const scale = viewWidth / width; const re = centerX + (sx - width / 2) * scale; const im = centerY + (sy - height / 2) * scale

Converts pixel coordinates to complex plane coordinates

Line by Line:

const scale = viewWidth / width
Calculates how many complex-plane units each pixel represents
const re = centerX + (sx - width / 2) * scale
Maps pixel x-coordinate to real part: offset from center by scaled pixel distance
const im = centerY + (sy - height / 2) * scale
Maps pixel y-coordinate to imaginary part: offset from center by scaled pixel distance
return { re, im }
Returns object with real and imaginary parts of the complex number

mousePressed()

mousePressed() initializes drag state. We don't know yet if this will be a click or drag, so we store initial values and let mouseDragged() and mouseReleased() determine the action.

function mousePressed() {
  if (
    mouseButton === LEFT &&
    mouseX >= 0 && mouseX < width &&
    mouseY >= 0 && mouseY < height
  ) {
    isDragging = true;
    hasDragged = false;
    dragStartX = mouseX;
    dragStartY = mouseY;
    dragStartCenterX = centerX;
    dragStartCenterY = centerY;
  }
}

πŸ”§ Subcomponents:

conditional Canvas Bounds Check if (mouseButton === LEFT && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height)

Ensures click is within canvas and is left mouse button

Line by Line:

if (mouseButton === LEFT && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height)
Checks that it's a left-click within canvas bounds
isDragging = true
Sets flag indicating a drag operation may be starting
hasDragged = false
Initializes flag to distinguish between click and drag
dragStartX = mouseX; dragStartY = mouseY
Records initial mouse position for drag calculation
dragStartCenterX = centerX; dragStartCenterY = centerY
Records view center at start of drag to calculate pan offset

mouseDragged()

mouseDragged() pans the view. The 2-pixel threshold prevents accidental drags from clicks. The scale calculation ensures that dragging moves the view proportionally to the current zoom level.

function mouseDragged() {
  if (!isDragging) return;

  const dx = mouseX - dragStartX;
  const dy = mouseY - dragStartY;
  if (!hasDragged && (abs(dx) > 2 || abs(dy) > 2)) {
    hasDragged = true;
  }

  const scale = viewWidth / width;
  centerX = dragStartCenterX - dx * scale;
  centerY = dragStartCenterY - dy * scale;
  viewDirty = true;
}

πŸ”§ Subcomponents:

conditional Drag Threshold Detection if (!hasDragged && (abs(dx) > 2 || abs(dy) > 2)) { hasDragged = true; }

Requires 2+ pixel movement to register as drag (prevents accidental drags)

calculation Pan Calculation const scale = viewWidth / width; centerX = dragStartCenterX - dx * scale; centerY = dragStartCenterY - dy * scale

Converts pixel drag distance to complex plane pan offset

Line by Line:

if (!isDragging) return
Exits if no drag is in progress
const dx = mouseX - dragStartX; const dy = mouseY - dragStartY
Calculates pixel distance moved since drag started
if (!hasDragged && (abs(dx) > 2 || abs(dy) > 2)) { hasDragged = true; }
Confirms drag after 2+ pixel movement to avoid registering tiny movements as drags
const scale = viewWidth / width
Calculates complex-plane units per pixel
centerX = dragStartCenterX - dx * scale; centerY = dragStartCenterY - dy * scale
Updates view center: subtract scaled drag distance (inverted because dragging right pans left)
viewDirty = true
Marks fractal for recomputation due to view change

mouseReleased()

mouseReleased() distinguishes clicks from drags. If hasDragged is false, it was a click, so we zoom in 2x at the cursor location. The 1e-12 limit prevents numerical underflow in complex arithmetic.

function mouseReleased() {
  if (!isDragging || mouseButton !== LEFT) return;

  isDragging = false;

  // If it was a click (not a drag), zoom in at cursor
  if (!hasDragged &&
      mouseX >= 0 && mouseX < width &&
      mouseY >= 0 && mouseY < height) {
    const c = screenToComplex(mouseX, mouseY);
    centerX = c.re;
    centerY = c.im;

    // Zoom in 2x
    viewWidth *= 0.5;
    viewWidth = max(viewWidth, 1e-12); // avoid zero
    viewDirty = true;
  }
}

πŸ”§ Subcomponents:

conditional Click vs Drag Detection if (!hasDragged && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height)

Distinguishes between click and drag; only zooms if it was a click

calculation Zoom Calculation viewWidth *= 0.5; viewWidth = max(viewWidth, 1e-12)

Zooms in 2x (halves view width) with minimum limit to prevent underflow

Line by Line:

if (!isDragging || mouseButton !== LEFT) return
Exits if no drag in progress or not left mouse button
isDragging = false
Clears dragging flag
if (!hasDragged && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height)
Checks if this was a click (no drag) within canvas bounds
const c = screenToComplex(mouseX, mouseY)
Converts click position to complex plane coordinates
centerX = c.re; centerY = c.im
Moves view center to clicked location
viewWidth *= 0.5
Zooms in 2x by halving the view width
viewWidth = max(viewWidth, 1e-12)
Prevents view width from becoming zero or negative (1e-12 is practical limit)
viewDirty = true
Marks fractal for recomputation due to zoom change

mouseWheel(event)

mouseWheel() implements smooth zoom around the cursor. The key insight: after changing view width, we recalculate where the cursor points to and adjust the center so that point stays fixed. This creates intuitive zoom behavior.

function mouseWheel(event) {
  if (mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height) {
    return;
  }

  const zoomFactor = event.delta > 0 ? 1.1 : 0.9;

  // Complex coordinate under mouse before zoom
  const before = screenToComplex(mouseX, mouseY);

  // Adjust view width
  viewWidth *= zoomFactor;
  viewWidth = constrain(viewWidth, 1e-12, BASE_VIEW_WIDTH * 10);

  // Re-center so the same complex point stays under the cursor
  const scale = viewWidth / width;
  centerX = before.re - (mouseX - width / 2) * scale;
  centerY = before.im - (mouseY - height / 2) * scale;

  viewDirty = true;

  // Prevent page scroll
  return false;
}

πŸ”§ Subcomponents:

conditional Canvas Bounds Check if (mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height) { return; }

Only zooms if mouse is over canvas

calculation Zoom Direction const zoomFactor = event.delta > 0 ? 1.1 : 0.9

Determines zoom in (1.1x) or out (0.9x) based on scroll direction

calculation Point Preservation const before = screenToComplex(mouseX, mouseY); ... centerX = before.re - (mouseX - width / 2) * scale; centerY = before.im - (mouseY - height / 2) * scale

Keeps the same complex point under the cursor after zoom

Line by Line:

if (mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height) { return; }
Ignores scroll wheel if mouse is outside canvas
const zoomFactor = event.delta > 0 ? 1.1 : 0.9
Scroll down (positive delta) = zoom out 1.1x; scroll up (negative delta) = zoom in 0.9x
const before = screenToComplex(mouseX, mouseY)
Records complex coordinate under cursor before zoom
viewWidth *= zoomFactor
Adjusts view width by zoom factor
viewWidth = constrain(viewWidth, 1e-12, BASE_VIEW_WIDTH * 10)
Clamps view width between minimum (1e-12) and maximum (35x initial view)
const scale = viewWidth / width
Recalculates scale after zoom
centerX = before.re - (mouseX - width / 2) * scale; centerY = before.im - (mouseY - height / 2) * scale
Re-centers view so the same complex point stays under cursor after zoom
return false
Prevents browser from scrolling the page

windowResized()

windowResized() is called automatically when the browser window is resized. It ensures the canvas and all data structures adapt to the new size, and marks the fractal for recomputation.

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

Line by Line:

resizeCanvas(windowWidth, windowHeight)
Resizes p5.js canvas to match new window dimensions
pixelDensity(1)
Resets pixel density to 1 for consistent rendering
resizeBuffers()
Reallocates fractal image and iteration data for new canvas size

log10(x)

JavaScript's Math.log() calculates natural logarithm (base e). To get base-10 log, we divide by Math.LN10 (the natural log of 10). This is used for displaying zoom level in scientific notation.

function log10(x) {
  return Math.log(x) / Math.LN10;
}

Line by Line:

return Math.log(x) / Math.LN10
Calculates base-10 logarithm by dividing natural log by ln(10)

πŸ“¦ Key Variables

centerX number

Stores the real (horizontal) coordinate of the view center in the complex plane

let centerX = -0.5;
centerY number

Stores the imaginary (vertical) coordinate of the view center in the complex plane

let centerY = 0.0;
BASE_VIEW_WIDTH number

Constant defining the initial horizontal span of the view in complex-plane units (3.5 units)

const BASE_VIEW_WIDTH = 3.5;
viewWidth number

Current horizontal span of the view in complex-plane units; decreases when zooming in

let viewWidth = BASE_VIEW_WIDTH;
fractalImg p5.Image

Stores the colored fractal image that gets displayed each frame

let fractalImg;
iterData Float32Array

Typed array storing normalized escape-time values (0..1) or -1 for interior points; one value per pixel

let iterData;
viewDirty boolean

Flag indicating whether the fractal needs recomputation due to zoom/pan changes

let viewDirty = true;
isDragging boolean

Flag indicating whether a mouse drag operation is currently in progress

let isDragging = false;
dragStartX number

Stores the initial mouse x-coordinate when a drag begins

let dragStartX = 0;
dragStartY number

Stores the initial mouse y-coordinate when a drag begins

let dragStartY = 0;
dragStartCenterX number

Stores the view center x-coordinate at the start of a drag

let dragStartCenterX = 0;
dragStartCenterY number

Stores the view center y-coordinate at the start of a drag

let dragStartCenterY = 0;
hasDragged boolean

Flag distinguishing between a click and a drag; true only if mouse moved >2 pixels

let hasDragged = false;
paletteOffset number

Animates the color palette by shifting hue values; cycles 0..1 continuously

let paletteOffset = 0;
PALETTE_SPEED number

Constant controlling how fast the color palette animates (0.003 per frame)

const PALETTE_SPEED = 0.003;

πŸ§ͺ Try This!

Experiment with the code by making these changes:

  1. Change BASE_VIEW_WIDTH from 3.5 to 2.0 to start with a zoomed-in view of the Mandelbrot set
  2. Modify PALETTE_SPEED from 0.003 to 0.01 to make the color animation 3x faster
  3. In computeMaxIterations(), change the base iterations from 60 to 100 to reveal more fine detail at all zoom levels
  4. In paletteColor(), change t * 0.7 to t * 1.5 to make the color bands wider and less swirly
  5. Modify the zoom factor in mouseReleased() from 0.5 (2x zoom) to 0.25 (4x zoom) for more aggressive zooming
  6. In mouseWheel(), change the zoom factors from 1.1 and 0.9 to 1.2 and 0.8 for more dramatic scroll-wheel zoom
  7. Add a line like fill(255, 0, 0); before circle(mouseX, mouseY, 10); in draw() to visualize where your cursor points in the complex plane
  8. Change the escape radius in computeFractal() from 4 to 16 to explore different fractal variations
  9. Modify the smooth coloring formula by changing the 1 in iter + 1 - nu to 0 or 2 to see how it affects color banding
Open in Editor & Experiment β†’

πŸ”§ Potential Improvements

Here are some ways this code could be enhanced:

PERFORMANCE computeFractal()

The nested loop iterates through every pixel every time the view changes, which can be slow at high resolutions. For a 1920x1080 canvas, that's 2 million pixels Γ— up to 600 iterations = 1.2 billion operations.

πŸ’‘ Implement multi-threaded rendering using Web Workers to compute the fractal in the background without blocking the UI. Alternatively, use a coarser grid initially and refine it progressively.

BUG computeFractal() - Mandelbrot iteration

The iteration order is incorrect: zy is calculated before zx, but zx uses the old zy value. This works by accident because of how complex multiplication works, but it's confusing and non-standard.

πŸ’‘ Store the old zx value: let zx_old = zx; then calculate zy = 2 * zx_old * zy + cy; zx = zx2 - zy2 + cx; This makes the logic clearer and matches standard Mandelbrot implementations.

STYLE computeFractal() and recolorFractal()

The separation between escape-time computation and coloring is good, but the code could be clearer about why iterData stores -1 for interior points. The sentinel value -1 is a common pattern but undocumented.

πŸ’‘ Add a comment explaining: 'iterData stores normalized escape-time (0..1) for exterior points, or -1 as a sentinel value for interior points (part of the set)'. This helps future readers understand the data format.

FEATURE draw() and drawHUD()

The HUD only displays zoom level; it doesn't show the current center coordinates or provide feedback about iteration depth.

πŸ’‘ Add lines to display centerX, centerY, and computeMaxIterations() in the HUD. This helps users understand where they are in the complex plane and why rendering might be slow at deep zooms.

BUG paletteColor()

The sqrt transformation t = Math.sqrt(t) is applied before hue calculation, but this permanently modifies t. If you later wanted to use the original t value, it's lost.

πŸ’‘ Use a separate variable: let t_adjusted = Math.sqrt(t); then use t_adjusted in the hue calculation. This preserves the original normalized value.

PERFORMANCE recolorFractal()

Every frame, the entire pixel array is rewritten even if only the palette offset changed. For a 1920x1080 canvas, that's 8+ million array writes per frame.

πŸ’‘ Consider using a shader-based approach with WebGL, or pre-compute a color lookup table and index into it. However, for most displays this is acceptable since modern browsers optimize array writes.

FEATURE mouseWheel()

The maximum zoom is capped at BASE_VIEW_WIDTH * 10 (35 units), but users might want to zoom out further to see the entire set.

πŸ’‘ Change the upper bound to BASE_VIEW_WIDTH * 100 or remove the upper limit entirely. Add a reset button (press 'R') to return to the initial view.

STYLE Global variables

Many interaction state variables (isDragging, hasDragged, dragStartX, etc.) are scattered as globals. This makes the code harder to maintain and test.

πŸ’‘ Group related variables into an object: let dragState = { isDragging: false, hasDragged: false, startX: 0, startY: 0, startCenterX: 0, startCenterY: 0 }; Then reference as dragState.isDragging, etc.

Preview

AI Fractal Explorer - Infinite Mandelbrot Zoom Explore infinite mathematical beauty! Click to zoom - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Fractal Explorer - Infinite Mandelbrot Zoom Explore infinite mathematical beauty! Click to zoom - Code flow showing setup, resizebuffers, computemaxiterations, computefractal, palettecolor, hsvtorgb, recolorfractal, draw, drawhud, screentocomplex, mousepressed, mousedragged, mousereleased, mousewheel, windowresized, log10
Code Flow Diagram