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

This sketch renders the Mandelbrot set by testing every pixel on screen against a mathematical formula, then colors the result with an animated psychedelic palette. Clicking zooms in 2x at the cursor, dragging pans around, and scrolling zooms smoothly - turning the canvas into an infinite fractal exploration tool.

🧪 Try This!

Experiment with the code by making these changes:

  1. Zoom in harder per click — Makes each click zoom in 4x instead of 2x, so you dive into the fractal much faster.
  2. Speed up the color swirl — Increases how fast the psychedelic palette cycles through hues every frame.
  3. Change the escape threshold — Raising the escape radius changes how smoothly colors blend near the fractal's boundary.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch draws the Mandelbrot set - one of the most famous fractals in mathematics - by iterating a complex number equation for every single pixel and coloring it based on how quickly that pixel 'escapes' to infinity. It teaches pixel-level image manipulation with loadPixels()/updatePixels(), complex-number arithmetic done manually with plain floats, smooth continuous coloring instead of banded colors, and HSV-to-RGB color conversion for a swirling animated palette. Interaction is handled through mousePressed, mouseDragged, mouseReleased and mouseWheel so you can click to zoom, drag to pan, and scroll to zoom smoothly around the cursor.

The code separates 'compute' from 'render': computeFractal() does the expensive math once per view and stores results in a Float32Array, while recolorFractal() cheaply repaints those stored values with a shifting color palette every frame - so the picture keeps animating even when you aren't zooming. By studying this file you'll learn how to cache expensive calculations, how escape-time algorithms produce fractal detail, and how mouse events can drive coordinate transforms between screen space and mathematical space.

⚙️ How It Works

  1. On load, setup() creates a full-window canvas, sets pixelDensity(1) for predictable pixel math, allocates image and data buffers, and computes the fractal once for the starting view.
  2. Every frame, draw() checks a 'viewDirty' flag - if the view has changed (zoomed or panned), it recalculates the entire fractal; otherwise it skips the expensive math.
  3. draw() then advances paletteOffset slightly and calls recolorFractal(), which reads the cached escape-time values and converts each one into an animated RGB color via HSV, so the fractal appears to shimmer even while stationary.
  4. Clicking without dragging zooms in 2x centered on the cursor by shrinking viewWidth and recentering on the clicked complex coordinate.
  5. Dragging the mouse pans the view by converting pixel movement into complex-plane movement using the current scale factor.
  6. Scrolling the mouse wheel zooms in or out by a small factor while re-centering the view so the point under the cursor stays fixed, creating a smooth zoom-toward-cursor feel.

🎓 Concepts You'll Learn

Escape-time fractal algorithmsComplex number iteration without a libraryPixel array manipulation (loadPixels/updatePixels)HSV to RGB color conversionScreen-to-world coordinate mappingCaching expensive computation vs cheap per-frame recoloringMouse interaction: click vs drag detection

📝 Code Breakdown

log10()

Small utility functions like this keep the rest of the code readable by giving a mathematical operation a clear name.

function log10(x) {
  return Math.log(x) / Math.LN10;
}
Line-by-line explanation (1 lines)
return Math.log(x) / Math.LN10;
JavaScript's Math object has no built-in log10, so this divides the natural log by the natural log of 10 to convert bases - a common math trick.

setup()

setup() runs once and prepares all the buffers the rest of the sketch depends on - getting pixelDensity right here is critical for pixel-perfect math later.

function setup() {
  createCanvas(windowWidth, windowHeight);
  pixelDensity(1); // Important for consistent pixel math
  resizeBuffers();
  computeFractal(); // Initial render
}
Line-by-line explanation (4 lines)
createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window.
pixelDensity(1); // Important for consistent pixel math
Forces exactly one canvas pixel per screen pixel - without this, high-DPI/Retina screens would double the pixel grid and break the 1-to-1 mapping between screen coordinates and the iterData array.
resizeBuffers();
Creates the image and Float32Array buffers sized to match the canvas.
computeFractal(); // Initial render
Runs the expensive Mandelbrot calculation once immediately so there's something to see on the very first frame.

resizeBuffers()

Separating the raw math (iterData) from the colored image (fractalImg) is what lets the palette animate every frame without redoing the slow Mandelbrot iteration.

function resizeBuffers() {
  fractalImg = createImage(width, height);
  iterData = new Float32Array(width * height);
  viewDirty = true;
}
Line-by-line explanation (3 lines)
fractalImg = createImage(width, height);
Creates a blank p5.Image the same size as the canvas that will hold the colored fractal pixels.
iterData = new Float32Array(width * height);
Allocates a flat typed array with one slot per pixel to store the raw escape-time value - Float32Array is faster and more memory-efficient than a normal JS array for this many numbers.
viewDirty = true;
Flags that the fractal must be recomputed, since the buffer sizes just changed.

computeMaxIterations()

Adaptive iteration depth is a key optimization in fractal renderers - using a fixed high iteration count everywhere would waste time on the wide starting view where it isn't needed.

🔬 This formula controls detail vs speed. What happens to fine detail at deep zoom if you raise the upper cap in constrain(maxIter, 60, 600) to 2000? What about frame rate?

  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);
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;
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Iteration Count Formula let maxIter = 60 + Math.floor(logZoom * 80);

Increases the iteration budget as you zoom in deeper so fine detail doesn't get muddy

const zoom = BASE_VIEW_WIDTH / viewWidth;
Computes how many times more zoomed-in the current view is compared to the starting view.
const logZoom = max(0, log10(zoom + 1));
Takes the logarithm of the zoom level so iteration count grows slowly and predictably instead of exploding; max(0, ...) prevents negative values when barely zoomed.
let maxIter = 60 + Math.floor(logZoom * 80);
Starts with a baseline of 60 iterations and adds more the deeper you zoom, since deep zooms need more iterations to resolve detail.
maxIter = constrain(maxIter, 60, 600);
Clamps the result so it never dips below 60 (blurry) or exceeds 600 (too slow to compute every frame).
return maxIter;
Hands the iteration limit back to computeFractal() to use in its escape-time loop.

computeFractal()

This function is the mathematical heart of the sketch - it runs the escape-time algorithm for every pixel and stores a normalized 'how fast did it escape' value, which is later turned into color separately so the palette can animate cheaply.

🔬 This while loop is the actual fractal formula z = z^2 + c repeated over and over. What happens visually if you add a third term, like always adding +0.1 to zx each iteration, to distort the classic Mandelbrot shape?

      while (iter < maxIter && zx2 + zy2 <= escapeRadiusSquared) {
        zy = 2 * zx * zy + cy;
        zx = zx2 - zy2 + cx;
        zx2 = zx * zx;
        zy2 = zy * zy;
        iter++;
      }
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;
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

for-loop Row Loop for (let py = 0; py < h; py++) {

Walks down every row of pixels on the canvas

for-loop Column Loop for (let px = 0; px < w; px++) {

Walks across every pixel in the current row

while-loop Escape-Time Iteration while (iter < maxIter && zx2 + zy2 <= escapeRadiusSquared) {

Repeatedly applies the Mandelbrot formula z = z^2 + c until the point escapes or the iteration budget runs out

conditional Interior vs Escaped if (iter === maxIter) {

Decides whether the point is likely inside the Mandelbrot set (never escaped) or escaped and needs smooth coloring

const scale = viewWidth / w; // same for x and y to preserve aspect
Converts one pixel's width into complex-plane units - using the same scale for x and y keeps the fractal from looking stretched.
const maxIter = computeMaxIterations();
Gets the current iteration budget, which grows as you zoom in deeper.
const cy = centerY + (py - h / 2) * scale;
Converts the pixel row into the corresponding imaginary-axis coordinate in the complex plane, centered on centerY.
const cx = centerX + (px - w / 2) * scale;
Converts the pixel column into the corresponding real-axis coordinate, centered on centerX.
while (iter < maxIter && zx2 + zy2 <= escapeRadiusSquared) {
The core fractal test: keep squaring and adding c until either the point 'escapes' past a radius of 2 (radius-squared 4) or we run out of iterations.
zy = 2 * zx * zy + cy;
Computes the imaginary part of z^2 + c using the previous z values (order matters - zx must still hold its old value here).
zx = zx2 - zy2 + cx;
Computes the real part of z^2 + c using zx2 and zy2 which were calculated from the previous iteration.
zx2 = zx * zx;
Caches zx squared so it doesn't need to be recalculated multiple times per loop, which matters a lot when this runs millions of times per frame.
if (iter === maxIter) {
If the loop used its entire iteration budget without escaping, the point is treated as inside the Mandelbrot set.
data[index] = -1;
Marks interior points with -1 so paletteColor() knows to paint them solid black.
const log_zn = Math.log(zx2 + zy2) / 2;
Part of the smooth coloring formula - measures how far past the escape radius the point actually flew.
let t = smoothIter / maxIter;
Normalizes the smooth iteration count into a 0-1 range so paletteColor() can map it to a color regardless of how high maxIter currently is.

paletteColor()

This function turns an abstract number (how fast a point escaped) into a visible color, which is the step that actually makes fractals beautiful rather than just mathematically interesting.

🔬 What happens if you remove the Math.sqrt(t) line entirely - do the colors look more evenly spread, or more bunched up near the fractal's edge?

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

  const hue = (paletteOffset + t * 0.7) % 1; // swirl through hues
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);
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Interior Color Check if (t < 0) {

Paints points inside the Mandelbrot set solid black instead of running them through the color palette

if (t < 0) {
Recall computeFractal() stored -1 for interior points - this catches that special case.
t = Math.sqrt(t); // Emphasize mid-tones
Applies a square root curve to the 0-1 value, stretching out mid-range values so more color variety shows up instead of everything clustering near 0 or 1.
const hue = (paletteOffset + t * 0.7) % 1; // swirl through hues
Combines the animated global offset with the pixel's own escape value to pick a hue, then wraps it with % 1 since hue is circular (0 and 1 are the same color).
return hsvToRgb(hue, sat, val);
Converts the hue/saturation/value triple into an RGB color object that can be written into the image's pixel array.

hsvToRgb()

This is a standard HSV-to-RGB algorithm. Using HSV instead of RGB directly makes it much easier to 'rotate through hues' smoothly, since you only need to change one number (hue) instead of juggling three (r, g, b).

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)
  };
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

switch-case Hue Sector Selector switch (i % 6) {

The color wheel is divided into 6 sectors of 60 degrees each; this picks which formula applies to the current hue

const i = Math.floor(h * 6);
Determines which of the 6 color sectors (red-yellow, yellow-green, etc.) the hue falls into.
const f = h * 6 - i;
Gets the fractional position within that sector, used to interpolate between two colors.
switch (i % 6) {
The %6 handles the edge case where h is exactly 1.0, wrapping it back to sector 0 instead of an invalid sector 6.
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
HSV math works in the 0-1 range, but pixel colors need 0-255 integers, so this scales and rounds each channel.

recolorFractal()

By keeping the expensive Mandelbrot math in iterData and only redoing this cheap coloring pass every frame, the sketch achieves an animated palette without recalculating fractal geometry 60 times a second.

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();
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Pixel Recolor Loop for (let i = 0; i < len; i++) {

Walks through every cached escape-time value and writes a fresh color into the image's pixel buffer

fractalImg.loadPixels();
Unlocks direct access to the image's raw pixel array so it can be written to manually - required before touching the pixels[] array.
const pixels = fractalImg.pixels;
Grabs a reference to the flat array of color values; each pixel takes 4 slots (red, green, blue, alpha).
const col = paletteColor(t);
Converts the cached escape-time number into an RGB color using the current animated palette offset.
pixels[pIndex++] = col.r;
Writes the red channel then advances the index by one, ready for green next - the ++ happens after the assignment.
fractalImg.updatePixels();
Pushes the manually-edited pixel array back into the image so it actually renders with the new colors.

draw()

draw() runs continuously and here it deliberately does two different kinds of work: expensive math only when needed (guarded by viewDirty), and cheap recoloring every single frame for smooth animation.

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();
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Recompute Check if (viewDirty) {

Only re-runs the expensive Mandelbrot math when the view has actually changed (zoomed/panned/resized)

if (viewDirty) { computeFractal(); }
Skips the slow per-pixel escape-time calculation on frames where nothing moved, which is what keeps panning and zooming feeling responsive.
paletteOffset = (paletteOffset + PALETTE_SPEED) % 1;
Nudges the color offset forward every frame and wraps it back to 0 once it passes 1, creating a continuous color cycle.
recolorFractal();
Repaints the cached escape-time data with the newly shifted palette, even though the underlying fractal shape hasn't changed.
image(fractalImg, 0, 0, width, height);
Draws the freshly colored image onto the canvas, stretched to fill the full width and height.

drawHUD()

push()/pop() are essential whenever you temporarily change drawing styles - they let you sandbox style changes to one section of code without manually resetting every property afterward.

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();
}
Line-by-line explanation (6 lines)
push();
Saves the current drawing style settings so changes made here (fill, textFont, etc.) don't leak into the fractal rendering.
fill(0, 160);
Sets a semi-transparent black fill (160 out of 255 alpha) for a translucent info panel background.
rect(10, 10, boxWidth, boxHeight, 4);
Draws the HUD background box with slightly rounded corners (the last argument is corner radius).
const logZoom = zoom > 0 ? log10(zoom) : 0;
Calculates the zoom level as a power of 10 for display, guarding against log10(0) which would be -Infinity.
text(`Zoom: 10^${nf(logZoom, 1, 2)}`, 18, 44);
Displays the current zoom level formatted to 2 decimal places using p5's nf() number-formatting helper.
pop();
Restores the drawing style saved by push(), so anything drawn after this function is unaffected by the HUD's fill/text settings.

screenToComplex()

This is the reverse of the math used inside computeFractal()'s pixel loops - it's reused by both click-to-zoom and scroll-to-zoom so a single formula keeps mouse coordinates and fractal coordinates in sync.

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 };
}
Line-by-line explanation (3 lines)
const scale = viewWidth / width;
Figures out how many complex-plane units one screen pixel represents at the current zoom level.
const re = centerX + (sx - width / 2) * scale;
Converts a screen x-coordinate into the real part of a complex number, relative to the current view center.
return { re, im };
Returns both coordinates together as an object so callers get a matched pair rather than two separate return values.

mousePressed()

mousePressed() only records starting state - it doesn't move anything yet. The real work happens in mouseDragged() and mouseReleased() based on what's recorded here.

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;
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

conditional Left Click Inside Canvas Check if ( mouseButton === LEFT && mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height ) {

Only starts a potential drag/click if the left mouse button was used and the cursor is actually over the canvas

isDragging = true;
Flags that a drag interaction has begun - mouseDragged() checks this flag before doing anything.
hasDragged = false;
Resets the flag that distinguishes a click from a drag; it will only flip to true if the mouse actually moves a noticeable amount.
dragStartCenterX = centerX;
Remembers where the view was centered before this drag started, so panning math can be based on the original position rather than accumulating drift.

mouseDragged()

This function shows a common pattern in mouse interaction: continuously recalculating movement relative to a fixed starting point (dragStartCenterX) rather than accumulating tiny per-frame deltas, which avoids drift errors.

🔬 This 2-pixel threshold decides whether a mouse-down/up counts as a 'click' (zoom) or a 'drag' (pan). What happens if you raise it to 30 - does it get harder to accidentally pan instead of zoom?

  const dx = mouseX - dragStartX;
  const dy = mouseY - dragStartY;
  if (!hasDragged && (abs(dx) > 2 || abs(dy) > 2)) {
    hasDragged = true;
  }
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;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Drag Threshold Check if (!hasDragged && (abs(dx) > 2 || abs(dy) > 2)) {

Distinguishes a real drag from a tiny accidental mouse jitter during a click, using a 2-pixel threshold

if (!isDragging) return;
Guard clause - does nothing unless mousePressed() already started a valid drag inside the canvas.
const dx = mouseX - dragStartX;
Measures how far the mouse has moved horizontally since the drag began.
centerX = dragStartCenterX - dx * scale;
Moves the view center opposite to the mouse movement (subtracting) so the fractal appears to follow the mouse, like dragging a piece of paper.
viewDirty = true;
Marks that the view has changed so draw() knows to recompute the fractal on the next frame.

mouseReleased()

This function ties together click detection (using the hasDragged flag set elsewhere) with the same coordinate conversion used by the scroll-wheel zoom, so both interactions feel consistent.

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;
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

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

Only triggers the zoom-in behavior if the mouse never moved far enough to count as a drag

if (!isDragging || mouseButton !== LEFT) return;
Guard clause ignoring releases that didn't start as a valid left-button drag.
const c = screenToComplex(mouseX, mouseY);
Finds the exact complex-plane point under the cursor so the zoom can be centered precisely there.
viewWidth *= 0.5;
Halves the visible width of the complex plane, which doubles the effective zoom level.
viewWidth = max(viewWidth, 1e-12); // avoid zero
Prevents viewWidth from ever reaching exactly zero, which would cause division-by-zero errors elsewhere in the math.

mouseWheel()

Zooming toward the cursor (instead of the canvas center) requires solving for a new center that keeps one specific point fixed on screen - the same 'before/after' recentering trick shows up in maps, image viewers, and games.

🔬 What happens to the zoom feel if you make these factors more extreme, like 2.0 and 0.5, instead of the gentle 1.1/0.9?

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

  // Complex coordinate under mouse before zoom
  const before = screenToComplex(mouseX, mouseY);
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;
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

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

Picks whether to zoom out (positive delta, scroll down) or zoom in (negative delta, scroll up)

if (mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height) { return; }
Ignores scroll events that happen outside the canvas area.
const zoomFactor = event.delta > 0 ? 1.1 : 0.9;
event.delta is positive when scrolling down (away from you) - this maps that to zooming out slightly (1.1x wider view) and scrolling up to zooming in (0.9x narrower view).
const before = screenToComplex(mouseX, mouseY);
Records the exact fractal coordinate under the cursor before changing the zoom, so it can be kept in the same screen position afterward.
viewWidth = constrain(viewWidth, 1e-12, BASE_VIEW_WIDTH * 10);
Keeps the zoom level within sane bounds - never fully zero (breaks math) and never wider than 10x the starting view.
centerX = before.re - (mouseX - width / 2) * scale;
Recalculates the view center so that the same complex point that was under the cursor before zooming stays under the cursor after zooming - this is what makes the zoom feel like it's 'toward the cursor' rather than toward the screen center.
return false;
Tells the browser not to also scroll the page itself while zooming the fractal.

windowResized()

windowResized() is a built-in p5.js callback that fires automatically whenever the browser window changes size - essential for making a sketch feel responsive on any screen.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  pixelDensity(1);
  resizeBuffers();
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the actual canvas element whenever the browser window changes size.
pixelDensity(1);
Re-asserts a 1:1 pixel density in case the browser reset it during the resize.
resizeBuffers();
Recreates the image and Float32Array buffers to match the new canvas dimensions and flags the view as dirty so the fractal redraws at the new size.

📦 Key Variables

centerX number

The real (horizontal) part of the complex number at the center of the current view.

let centerX = -0.5;
centerY number

The imaginary (vertical) part of the complex number at the center of the current view.

let centerY = 0.0;
BASE_VIEW_WIDTH number

The width of the complex plane visible at zoom level 1x; used as the reference point for calculating current zoom depth.

const BASE_VIEW_WIDTH = 3.5;
viewWidth number

How wide the visible slice of the complex plane currently is - shrinking it zooms in, growing it zooms out.

let viewWidth = BASE_VIEW_WIDTH;
fractalImg object

A p5.Image buffer that stores the final colored fractal picture, redrawn to the canvas every frame with image().

let fractalImg;
iterData array

A Float32Array caching the normalized escape-time value (or -1 for interior) for every pixel, so color animation doesn't require redoing the Mandelbrot math.

let iterData;
viewDirty boolean

Flags whether the view has changed since the last fractal computation, so draw() knows whether to recompute the expensive math.

let viewDirty = true;
isDragging boolean

Tracks whether the mouse button is currently held down after being pressed inside the canvas.

let isDragging = false;
dragStartX number

The mouse's x position at the moment a drag began, used to measure how far the mouse has moved.

let dragStartX = 0;
dragStartY number

The mouse's y position at the moment a drag began.

let dragStartY = 0;
dragStartCenterX number

The view's centerX value at the moment a drag began, used as the base for panning calculations.

let dragStartCenterX = 0;
dragStartCenterY number

The view's centerY value at the moment a drag began.

let dragStartCenterY = 0;
hasDragged boolean

Becomes true once the mouse moves far enough during a press, distinguishing a drag/pan from a simple click/zoom.

let hasDragged = false;
paletteOffset number

A continuously increasing value (wrapped 0-1) that shifts the color palette's hue every frame to create the animated swirl effect.

let paletteOffset = 0;
PALETTE_SPEED number

How much paletteOffset increases per frame, controlling how fast the colors cycle.

const PALETTE_SPEED = 0.003;

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE mouseDragged()

Every single mouseDragged event sets viewDirty = true, which forces a full-canvas Mandelbrot recomputation (potentially hundreds of iterations per pixel) synchronously on the main thread during a drag, which can feel laggy on large screens or deep zooms.

💡 Throttle recomputation during drags (e.g. only recompute every few frames, or render at reduced resolution while dragging and refresh at full quality on mouseReleased) to keep panning smooth.

BUG computeFractal()

Because JavaScript numbers are standard double-precision floats, viewWidth eventually hits the limits of floating-point precision at extreme zoom depths (roughly 1e-13 to 1e-15), causing the fractal to become blocky, pixelated, or stop revealing new detail even though viewWidth keeps shrinking.

💡 For much deeper zooms, switch to an arbitrary-precision or 'perturbation theory' based Mandelbrot algorithm, or at least detect when precision is exhausted and stop allowing further zoom with a message.

FEATURE draw() / computeFractal()

The entire UI freezes while computeFractal() runs on the main thread, since it's a large nested loop with no yielding - on big canvases or high iteration counts this can cause a visible stutter every time you zoom or pan.

💡 Move computeFractal() into a Web Worker, or split the pixel loop across multiple animation frames (e.g. compute a few rows per frame) so the browser stays responsive and could show a progress indicator.

STYLE mouseWheel()

The zoomFactor of 1.1/0.9 is a magic number duplicated in spirit from viewWidth *= 0.5 in mouseReleased(), with no shared constant tying scroll-zoom speed to click-zoom speed.

💡 Extract these into named constants like SCROLL_ZOOM_IN_FACTOR and SCROLL_ZOOM_OUT_FACTOR near the top of the file alongside PALETTE_SPEED, making the zoom feel easier to tune in one place.

🔄 Code Flow

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

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> dirtycheck[Recompute Check] dirtycheck -->|true| computemaxiterations[computemaxiterations] computemaxiterations --> computefractal[computefractal] computefractal --> itercalc[Iter Count Formula] itercalc --> rowloop[Row Loop] rowloop --> colloop[Column Loop] colloop --> escapeloop[Escape-Time Iteration] escapeloop --> interiorcheck[Interior vs Escaped] interiorcheck -->|escaped| interiorcolorcheck[Interior Color Check] interiorcolorcheck --> hueSectorSwitch[Hue Sector Selector] hueSectorSwitch --> recolorloop[Pixel Recolor Loop] recolorloop --> draw interiorcheck -->|interior| draw dirtycheck -->|false| recolorfractal[recolorfractal] recolorfractal --> draw draw --> drawhud[drawhud] drawhud --> draw click setup href "#fn-setup" click draw href "#fn-draw" click dirtycheck href "#sub-dirty-check" click computemaxiterations href "#fn-computemaxiterations" click computefractal href "#fn-computefractal" click itercalc href "#sub-iter-calc" click rowloop href "#sub-row-loop" click colloop href "#sub-col-loop" click escapeloop href "#sub-escape-loop" click interiorcheck href "#sub-interior-check" click interiorcolorcheck href "#sub-interior-color-check" click hueSectorSwitch href "#sub-hue-sector-switch" click recolorloop href "#sub-recolor-loop" click recolorfractal href "#fn-recolorfractal" click drawhud href "#fn-drawhud"

❓ Frequently Asked Questions

What visual experience does the AI Fractal Explorer sketch provide?

The AI Fractal Explorer creates stunning, infinite zooms into vibrant Mandelbrot fractals, showcasing intricate mathematical patterns and psychedelic color palettes.

How can users interact with the AI Fractal Explorer sketch?

Users can click to zoom in, drag to pan the view, and use the mouse wheel to zoom in or out around the cursor, allowing for an engaging exploration of the fractal landscape.

What creative coding concepts are demonstrated in the AI Fractal Explorer?

This sketch showcases procedural generation and complex arithmetic through pixel-by-pixel rendering of the Mandelbrot set, along with dynamic color palette cycling.

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 log10, setup, resizebuffers, computemaxiterations, computefractal, palettecolor, hsvtorgb, recolorfractal, draw, drawhud, screentocomplex, mousepressed, mousedragged, mousereleased, mousewheel, windowresized
Code Flow Diagram