Physics Lab

Fractals & Chaos

Explore self-similar patterns emerging from chaos. Visualize Mandelbrot, Julia, Newton fractals and the legendary Chaos Game.

Iterated Function System (IFS)

Start at a random point and repeatedly apply random transformations. A fractal emerges from the chaos!

2.0M
Points Plotted
0
Generations
0
Time (ms)
0

Mandelbrot Set

The most famous fractal: z = z² + c. Click+drag to zoom, right-click to zoom out. Double-click to zoom in on a point.

128
Center (Re)
-0.5
Center (Im)
0
Zoom
1x

Julia Sets

Same formula as Mandelbrot, but fix c and vary z. Drag the c point on the mini Mandelbrot map to explore different Julia sets.

128
c = -0.7+0.27i
c (Real)
-0.7
c (Imag)
0.27

Newton Fractal

Newton's method for z³ - 1 = 0. Colors show which root each point converges to. Stunning spiraling boundaries!

64
Polynomial
z³ - 1
Roots
3
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// Tab switching
    document.querySelectorAll('.tab').forEach(tab => {
      tab.addEventListener('click', () => {
        const tabName = tab.dataset.tab;
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
        tab.classList.add('active');
        document.getElementById(tabName).classList.add('active');
      });
    });

    // ============ CHAOS GAME ============
    const chaosCanvas = document.getElementById('chaosCanvas');
    const chaosCtx = chaosCanvas.getContext('2d', { willReadFrequently: true });
    const chaosStatus = document.getElementById('chaosStatus');

    const chaosPresets = {
      sierpinski: [
        { a: 0.5, b: 0, c: 0, d: 0.5, e: 0, f: 0 },
        { a: 0.5, b: 0, c: 0, d: 0.5, e: 0.5, f: 0 },
        { a: 0.5, b: 0, c: 0, d: 0.5, e: 0.25, f: 0.433 }
      ],
      fern: [
        { a: 0, b: 0, c: 0, d: 0.16, e: 0, f: 0, prob: 0.01 },
        { a: 0.85, b: 0.02, c: -0.02, d: 0.83, e: 0, f: 1.6, prob: 0.85 },
        { a: 0.2, b: -0.26, c: 0.23, d: 0.22, e: 0, f: 1.6, prob: 0.07 },
        { a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0, f: 0.44, prob: 0.07 }
      ],
      dragon: [
        { a: -0.5, b: -0.5, c: 0.5, d: -0.5, e: 0.5, f: 0.5 },
        { a: 0.5, b: 0.5, c: -0.5, d: 0.5, e: 0.5, f: 0.5 }
      ],
      carpet: [
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 0, f: 0 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 1/3, f: 0 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 2/3, f: 0 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 0, f: 1/3 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 2/3, f: 1/3 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 0, f: 2/3 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 1/3, f: 2/3 },
        { a: 1/3, b: 0, c: 0, d: 1/3, e: 2/3, f: 2/3 }
      ]
    };

    const chaosColorPalettes = {
      'transform': ['#ff2200', '#00c896', '#f5c518', '#1e90ff', '#ff69b4', '#00d9ff'],
      'frequency': null, // density based
      'rainbow': null,
      'fire': null
    };

    let chaosCurrentPreset = 'sierpinski';

    document.querySelectorAll('[data-preset]').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        chaosCurrentPreset = btn.dataset.preset;
      });
    });

    document.getElementById('chaosSamples').addEventListener('input', (e) => {
      document.getElementById('chaosSamplesValue').textContent = e.target.value + 'M';
    });

    function applyTransform(x, y, transform) {
      return {
        x: transform.a * x + transform.b * y + transform.e,
        y: transform.c * x + transform.d * y + transform.f
      };
    }

    function generateChaosGame() {
      const samples = parseFloat(document.getElementById('chaosSamples').value) * 1_000_000;
      const preset = chaosPresets[chaosCurrentPreset];
      const colorScheme = document.getElementById('chaosColorScheme').value;
      const speed = document.getElementById('chaosSpeed').value;

      const skipRate = speed === 'fast' ? 10 : speed === 'slow' ? 0.1 : 1;
      const actualSamples = Math.floor(samples * skipRate);

      chaosStatus.style.display = 'block';
      chaosStatus.textContent = 'Generating...';

      const start = performance.now();
      chaosCtx.fillStyle = '#000';
      chaosCtx.fillRect(0, 0, chaosCanvas.width, chaosCanvas.height);

      const imgData = chaosCtx.getImageData(0, 0, chaosCanvas.width, chaosCanvas.height);
      const data = imgData.data;

      let x = Math.random(), y = Math.random();
      const width = chaosCanvas.width;
      const height = chaosCanvas.height;

      for (let i = 0; i < actualSamples; i++) {
        const transform = preset[Math.floor(Math.random() * preset.length)];
        const p = applyTransform(x, y, transform);
        x = p.x;
        y = p.y;

        if (x >= 0 && x <= 1 && y >= 0 && y <= 1) {
          const px = Math.floor(x * width);
          const py = Math.floor((1 - y) * height);
          const idx = (py * width + px) * 4;

          if (colorScheme === 'transform') {
            const transIdx = preset.indexOf(transform);
            const colorPalette = chaosColorPalettes['transform'];
            const hexColor = colorPalette[transIdx % colorPalette.length];
            const rgb = parseInt(hexColor.slice(1), 16);
            data[idx] = (rgb >> 16) & 255;
            data[idx + 1] = (rgb >> 8) & 255;
            data[idx + 2] = rgb & 255;
            data[idx + 3] = 255;
          } else {
            data[idx] += 1;
            data[idx + 1] += 1;
            data[idx + 2] += 1;
            data[idx + 3] = 255;
          }
        }
      }

      chaosCtx.putImageData(imgData, 0, 0);
      const end = performance.now();

      document.getElementById('chaosPointsValue').textContent = actualSamples.toLocaleString();
      document.getElementById('chaosGenValue').textContent = actualSamples.toLocaleString();
      document.getElementById('chaosTimeValue').textContent = Math.round(end - start);

      chaosStatus.classList.add('success');
      chaosStatus.textContent = `✓ Rendered ${actualSamples.toLocaleString()} points in ${Math.round(end - start)}ms`;
    }

    document.getElementById('chaosGenerate').addEventListener('click', generateChaosGame);
    document.getElementById('chaosReset').addEventListener('click', () => {
      chaosCtx.fillStyle = '#000';
      chaosCtx.fillRect(0, 0, chaosCanvas.width, chaosCanvas.height);
    });
    document.getElementById('chaosSave').addEventListener('click', () => {
      const link = document.createElement('a');
      link.href = chaosCanvas.toDataURL();
      link.download = `chaos-${chaosCurrentPreset}-${Date.now()}.png`;
      link.click();
    });

    // ============ MANDELBROT ============
    const mandelbrotCanvas = document.getElementById('mandelbrotCanvas');
    const mandelbrotCtx = mandelbrotCanvas.getContext('2d', { willReadFrequently: true });
    const mandelbrotStatus = document.getElementById('mandelbrotStatus');

    let mandelbrotState = {
      centerRe: -0.5,
      centerIm: 0,
      zoom: 1
    };

    const mandelbrotLocations = {
      'full': { centerRe: -0.5, centerIm: 0, zoom: 1 },
      'seahorse': { centerRe: -0.7489, centerIm: 0.1008, zoom: 50 },
      'elephant': { centerRe: -0.7533, centerIm: 0.1138, zoom: 50 },
      'spiral': { centerRe: -0.747, centerIm: 0.1, zoom: 100 }
    };

    document.querySelectorAll('[data-location]').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('[data-location]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        const loc = mandelbrotLocations[btn.dataset.location];
        mandelbrotState = { ...loc };
        renderMandelbrot();
      });
    });

    document.getElementById('mandelbrotIterations').addEventListener('input', (e) => {
      document.getElementById('mandelbrotIterValue').textContent = e.target.value;
    });

    function mandelbrotColor(iterations, maxIter) {
      const t = iterations / maxIter;
      const hue = (t * 360) % 360;
      return `hsl(${hue}, 100%, 50%)`;
    }

    function renderMandelbrot() {
      const maxIter = parseInt(document.getElementById('mandelbrotIterations').value);
      const colorScheme = document.getElementById('mandelbrotColorScheme').value;
      const zoomLevel = parseFloat(document.getElementById('mandelbrotZoom').value);

      mandelbrotState.zoom = zoomLevel;

      mandelbrotStatus.style.display = 'block';
      mandelbrotStatus.textContent = 'Rendering...';

      const start = performance.now();
      const width = mandelbrotCanvas.width;
      const height = mandelbrotCanvas.height;
      const imgData = mandelbrotCtx.createImageData(width, height);
      const data = imgData.data;

      const pixelWidth = 4 / (mandelbrotState.zoom * Math.min(width, height));

      for (let py = 0; py < height; py++) {
        for (let px = 0; px < width; px++) {
          const re = mandelbrotState.centerRe + (px - width / 2) * pixelWidth;
          const im = mandelbrotState.centerIm + (py - height / 2) * pixelWidth;

          let zre = 0, zim = 0, iter = 0;
          let zre2 = 0, zim2 = 0;

          while (iter < maxIter && zre2 + zim2 < 4) {
            const temp = zre * zre - zim * zim + re;
            zim = 2 * zre * zim + im;
            zre = temp;
            zre2 = zre * zre;
            zim2 = zim * zim;
            iter++;
          }

          const idx = (py * width + px) * 4;
          if (iter === maxIter) {
            data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255;
          } else {
            const hue = (iter / maxIter * 360) % 360;
            const rgb = hslToRgb(hue, 100, 50);
            data[idx] = rgb.r; data[idx + 1] = rgb.g; data[idx + 2] = rgb.b; data[idx + 3] = 255;
          }
        }
      }

      mandelbrotCtx.putImageData(imgData, 0, 0);
      const end = performance.now();

      document.getElementById('mandelbrotCenterRe').textContent = mandelbrotState.centerRe.toFixed(4);
      document.getElementById('mandelbrotCenterIm').textContent = mandelbrotState.centerIm.toFixed(4);
      document.getElementById('mandelbrotZoomValue').textContent = (mandelbrotState.zoom * 100).toFixed(0) + '%';

      mandelbrotStatus.classList.add('success');
      mandelbrotStatus.textContent = `✓ Rendered in ${Math.round(end - start)}ms`;
    }

    function hslToRgb(h, s, l) {
      const c = (100 - Math.abs(2 * l - 100)) * s / 100;
      const x = c * (1 - Math.abs((h / 60) % 2 - 1));
      const m = l / 100 - c / 2;
      let r = 0, g = 0, b = 0;

      if (h < 60) { r = c; g = x; }
      else if (h < 120) { r = x; g = c; }
      else if (h < 180) { g = c; b = x; }
      else if (h < 240) { g = x; b = c; }
      else if (h < 300) { r = x; b = c; }
      else { r = c; b = x; }

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

    document.getElementById('mandelbrotRender').addEventListener('click', renderMandelbrot);
    document.getElementById('mandelbrotReset').addEventListener('click', () => {
      mandelbrotState = { centerRe: -0.5, centerIm: 0, zoom: 1 };
      document.getElementById('mandelbrotZoom').value = 1;
      renderMandelbrot();
    });
    document.getElementById('mandelbrotSave').addEventListener('click', () => {
      const link = document.createElement('a');
      link.href = mandelbrotCanvas.toDataURL();
      link.download = `mandelbrot-${Date.now()}.png`;
      link.click();
    });

    // ============ JULIA ============
    const juliaCanvas = document.getElementById('juliaCanvas');
    const juliaCtx = juliaCanvas.getContext('2d', { willReadFrequently: true });
    const juliaMandelbrotMap = document.getElementById('juliaMandelbrotMap');
    const juliaMandelbrotCtx = juliaMandelbrotMap.getContext('2d', { willReadFrequently: true });
    const juliaStatus = document.getElementById('juliaStatus');

    let juliaState = {
      cRe: -0.7,
      cIm: 0.27
    };

    const juliaPresets = {
      'classic': { cRe: -0.7, cIm: 0.27 },
      'spiral': { cRe: -0.162, cIm: 1.04 },
      'dendrite': { cRe: -0.8, cIm: 0.156 },
      'seahorse': { cRe: -0.162, cIm: 1.04 }
    };

    document.querySelectorAll('[data-juliapreset]').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('[data-juliapreset]').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        const preset = juliaPresets[btn.dataset.juliapreset];
        juliaState = { ...preset };
        renderJulia();
        renderJuliaMandelbrotMap();
      });
    });

    document.getElementById('juliaIterations').addEventListener('input', (e) => {
      document.getElementById('juliaIterValue').textContent = e.target.value;
    });

    function renderJulia() {
      const maxIter = parseInt(document.getElementById('juliaIterations').value);
      juliaStatus.style.display = 'block';
      juliaStatus.textContent = 'Rendering...';

      const start = performance.now();
      const width = juliaCanvas.width;
      const height = juliaCanvas.height;
      const imgData = juliaCtx.createImageData(width, height);
      const data = imgData.data;

      for (let py = 0; py < height; py++) {
        for (let px = 0; px < width; px++) {
          const zRe = (px / width) * 4 - 2;
          const zIm = (py / height) * 4 - 2;

          let re = zRe, im = zIm, iter = 0;
          while (iter < maxIter && re * re + im * im < 4) {
            const temp = re * re - im * im + juliaState.cRe;
            im = 2 * re * im + juliaState.cIm;
            re = temp;
            iter++;
          }

          const idx = (py * width + px) * 4;
          if (iter === maxIter) {
            data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255;
          } else {
            const rgb = hslToRgb((iter / maxIter * 360) % 360, 100, 50);
            data[idx] = rgb.r; data[idx + 1] = rgb.g; data[idx + 2] = rgb.b; data[idx + 3] = 255;
          }
        }
      }

      juliaCtx.putImageData(imgData, 0, 0);
      const end = performance.now();

      document.getElementById('juliaCReal').textContent = juliaState.cRe.toFixed(3);
      document.getElementById('juliaCImag').textContent = juliaState.cIm.toFixed(3);
      document.getElementById('juliaC').textContent = `${juliaState.cRe.toFixed(2)}${juliaState.cIm >= 0 ? '+' : ''}${juliaState.cIm.toFixed(2)}i`;

      juliaStatus.classList.add('success');
      juliaStatus.textContent = `✓ Rendered in ${Math.round(end - start)}ms`;
    }

    function renderJuliaMandelbrotMap() {
      const width = juliaMandelbrotMap.width;
      const height = juliaMandelbrotMap.height;
      const imgData = juliaMandelbrotCtx.createImageData(width, height);
      const data = imgData.data;

      for (let py = 0; py < height; py++) {
        for (let px = 0; px < width; px++) {
          const cRe = (px / width) * 4 - 2.5;
          const cIm = (py / height) * 4 - 2;

          let re = 0, im = 0, iter = 0;
          while (iter < 64 && re * re + im * im < 4) {
            const temp = re * re - im * im + cRe;
            im = 2 * re * im + cIm;
            re = temp;
            iter++;
          }

          const idx = (py * width + px) * 4;
          if (iter === 64) {
            data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255;
          } else {
            const rgb = hslToRgb((iter / 64 * 360) % 360, 100, 50);
            data[idx] = rgb.r; data[idx + 1] = rgb.g; data[idx + 2] = rgb.b; data[idx + 3] = 255;
          }
        }
      }

      juliaMandelbrotCtx.putImageData(imgData, 0, 0);

      // Draw marker at current c
      const cReMapped = ((juliaState.cRe + 2.5) / 4) * width;
      const cImMapped = ((juliaState.cIm + 2) / 4) * height;
      juliaMandelbrotCtx.strokeStyle = '#ff2200';
      juliaMandelbrotCtx.lineWidth = 2;
      juliaMandelbrotCtx.beginPath();
      juliaMandelbrotCtx.arc(cReMapped, cImMapped, 5, 0, Math.PI * 2);
      juliaMandelbrotCtx.stroke();
    }

    juliaMandelbrotMap.addEventListener('click', (e) => {
      const rect = juliaMandelbrotMap.getBoundingClientRect();
      const x = (e.clientX - rect.left) / rect.width;
      const y = (e.clientY - rect.top) / rect.height;
      juliaState.cRe = x * 4 - 2.5;
      juliaState.cIm = y * 4 - 2;
      renderJulia();
      renderJuliaMandelbrotMap();
    });

    document.getElementById('juliaRender').addEventListener('click', () => {
      renderJulia();
      renderJuliaMandelbrotMap();
    });
    document.getElementById('juliaReset').addEventListener('click', () => {
      juliaState = { cRe: -0.7, cIm: 0.27 };
      renderJulia();
      renderJuliaMandelbrotMap();
    });
    document.getElementById('juliaSave').addEventListener('click', () => {
      const link = document.createElement('a');
      link.href = juliaCanvas.toDataURL();
      link.download = `julia-${Date.now()}.png`;
      link.click();
    });

    // ============ NEWTON ============
    const newtonCanvas = document.getElementById('newtonCanvas');
    const newtonCtx = newtonCanvas.getContext('2d', { willReadFrequently: true });
    const newtonStatus = document.getElementById('newtonStatus');

    let newtonPower = 3;
    let newtonZoom = 1;

    document.getElementById('newtonPowerInput').addEventListener('change', (e) => {
      newtonPower = parseInt(e.target.value);
      document.getElementById('newtonPolyDisplay').textContent = `z^${newtonPower} - 1`;
      document.getElementById('newtonRootsCount').textContent = newtonPower;
    });

    document.getElementById('newtonIterations').addEventListener('input', (e) => {
      document.getElementById('newtonIterValue').textContent = e.target.value;
    });

    document.getElementById('newtonZoom').addEventListener('input', (e) => {
      newtonZoom = parseFloat(e.target.value);
    });

    function complexMult(a, b) {
      return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re };
    }

    function complexDiv(a, b) {
      const denom = b.re * b.re + b.im * b.im;
      return { re: (a.re * b.re + a.im * b.im) / denom, im: (a.im * b.re - a.re * b.im) / denom };
    }

    function polyValue(z, n) {
      let result = { re: 1, im: 0 };
      for (let i = 1; i < n; i++) {
        result = complexMult(result, z);
      }
      result.re -= 1;
      return result;
    }

    function polyDerivative(z, n) {
      let result = { re: 0, im: 0 };
      for (let i = 1; i < n; i++) {
        let term = { re: 1, im: 0 };
        for (let j = 1; j < i; j++) {
          term = complexMult(term, z);
        }
        result.re += i * term.re;
        result.im += i * term.im;
      }
      return result;
    }

    function renderNewton() {
      const maxIter = parseInt(document.getElementById('newtonIterations').value);
      newtonStatus.style.display = 'block';
      newtonStatus.textContent = 'Rendering...';

      const start = performance.now();
      const width = newtonCanvas.width;
      const height = newtonCanvas.height;
      const imgData = newtonCtx.createImageData(width, height);
      const data = imgData.data;

      const rootColors = ['#ff2200', '#00c896', '#1e90ff', '#f5c518', '#ff69b4', '#00d9ff', '#90ee90', '#dda0dd'];

      for (let py = 0; py < height; py++) {
        for (let px = 0; px < width; px++) {
          let z = {
            re: (px - width / 2) / (width / 4) / newtonZoom,
            im: (py - height / 2) / (height / 4) / newtonZoom
          };

          let root = 0;
          for (let iter = 0; iter < maxIter; iter++) {
            const f = polyValue(z, newtonPower);
            const fp = polyDerivative(z, newtonPower);
            const delta = complexDiv(f, fp);

            z.re -= delta.re;
            z.im -= delta.im;

            if (Math.sqrt(delta.re * delta.re + delta.im * delta.im) < 1e-6) {
              const angle = Math.atan2(z.im, z.re);
              root = Math.round((angle / (2 * Math.PI) + 0.5) * newtonPower) % newtonPower;
              break;
            }
          }

          const idx = (py * width + px) * 4;
          const colorHex = rootColors[root % rootColors.length];
          const rgb = parseInt(colorHex.slice(1), 16);
          data[idx] = (rgb >> 16) & 255;
          data[idx + 1] = (rgb >> 8) & 255;
          data[idx + 2] = rgb & 255;
          data[idx + 3] = 255;
        }
      }

      newtonCtx.putImageData(imgData, 0, 0);
      const end = performance.now();

      newtonStatus.classList.add('success');
      newtonStatus.textContent = `✓ Rendered in ${Math.round(end - start)}ms`;
    }

    document.getElementById('newtonRender').addEventListener('click', renderNewton);
    document.getElementById('newtonReset').addEventListener('click', () => {
      newtonZoom = 1;
      document.getElementById('newtonZoom').value = 1;
      renderNewton();
    });
    document.getElementById('newtonSave').addEventListener('click', () => {
      const link = document.createElement('a');
      link.href = newtonCanvas.toDataURL();
      link.download = `newton-z${newtonPower}-${Date.now()}.png`;
      link.click();
    });

    // Initial renders
    window.addEventListener('load', () => {
      generateChaosGame();
      renderMandelbrot();
      renderJulia();
      renderJuliaMandelbrotMap();
      renderNewton();
    });