<!-- Used to control the zoom level etc. -->
<div class="controls">
  <div>
    Zoom size:
    <input type="range" min="2" max="50" value="10" id="zoomsize">
  </div>

  <input type="button" id="reset" value="Reset">
</div>

<!-- A little box that shows what part of the Mandelbrot set will be shown on click -->
<div class="selector"></div>

<!-- The canvas we'll render the Mandelbrot set on -->
<canvas class="canvas" />
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}
.controls {
  position: fixed;
  background-color: #f0f0f0;
  z-index: 1000;
}
.selector {
  border: 2px solid #000;
  opacity: .2;
  position: fixed;
  z-index: 999;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.canvas {
  width: 100%;
  height: 100vh;
}
class Complex {
  constructor(real, imaginary) {
    this.real = real
    this.imaginary = imaginary
  }

  plus(other) {
    return new Complex(
      this.real + other.real,
      this.imaginary + other.imaginary
    )
  }

  times(other) {
    return new Complex(
      (this.real * other.real - this.imaginary * other.imaginary),
      (this.real * other.imaginary + other.real * this.imaginary)
    )
  }
}

/**
 * Calculates n+1
 */
const iterate = (n, c) => n.times(n).plus(c)

/**
 * Checks if a complex number `c` converges according to the Mandelbrot definition.
 */
const doesDiverge = (c, maxIter) => {
  let n = new Complex(0, 0)
  for (let i = 0; i < maxIter; i++) {
    n = iterate(n, c)
  }

  // If the iteration diverges, these values will be NaN quite fast. Around 100 iterations is needed.
  return isNaN(n.real) || isNaN(n.imaginary)
}

/**
 * Draws the Mandelbrot set.
 */
const drawMandelbrotSet = (realFrom, realTo, imagFrom, imagTo) => {
  const canvas = document.querySelector('canvas')
  const ctx = canvas.getContext('2d')

  const winWidth = window.innerWidth
  const winHeight = window.innerHeight

  // Reset the canvas
  canvas.width = winWidth
  canvas.height = winHeight
  ctx.clearRect(0, 0, winWidth, winHeight)

  // Determine how big a change in number a single pixel is
  const stepSizeReal = (realTo - realFrom) / winWidth
  const stepSizeImaginary = (imagTo - imagFrom) / winHeight

  // Loop through every pixel of the complex plane that is currently visible
  for (let x = realFrom; x <= realTo; x += stepSizeReal) {
    for (let y = imagFrom; y <= imagTo; y += stepSizeImaginary) {
      // Determine if this coordinate is part of the Mandelbrot set.
      const c = new Complex(x, y)
      const isInMandelbrotSet = !doesDiverge(c, 50)

      const r = isInMandelbrotSet ? 67 : 104
      const g = isInMandelbrotSet ? 65 : 211
      const b = isInMandelbrotSet ? 144 : 145

      // Cast the coordinates on the complex plane back to actual pixel coordinates
      const screenX = (x - realFrom) / (realTo - realFrom) * winWidth
      const screenY = (y - imagFrom) / (imagTo - imagFrom) * winHeight

      // Draw a single pixel
      ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
      ctx.fillRect(screenX, screenY, 1, 1)
    }
  }
}

// X coordinate
const realInitial = {
  from: -2,
  to: 2,
}

// Y coordinate, keep the aspect ratio
const imagInitial = {
  from: realInitial.from / window.innerWidth * window.innerHeight,
  to: realInitial.to / window.innerWidth * window.innerHeight,
}

// Ranging from negative to positive - which part of the plane is visible right now?
let real = realInitial
let imag = imagInitial

document.querySelector('#reset').addEventListener('click', () => {
  real = realInitial
  imag = imagInitial

  drawMandelbrotSet(real.from, real.to, imag.from, imag.to)
})


// Size of the zoom compared to current screen size
let zoomsize = 10

/**
 * Makes the selector follow the mouse
 */
document.addEventListener('mousemove', event => {
  const selector = document.querySelector('.selector')
  console.log(event.clientX, event.clientY)
  selector.style.top = `${event.clientY}px`
  selector.style.left = `${event.clientX}px`
  selector.style.width = `${window.innerWidth / zoomsize}px`
  selector.style.height = `${window.innerHeight / zoomsize}px`
})

/**
 * Zoom size adjustment.
 */
document.querySelector('#zoomsize').addEventListener('change', event => {
  zoomsize = parseInt(event.target.value)
})

/**
 * Perform a zoom
 */
document.querySelector('canvas').addEventListener('click', event => {
  const winWidth = window.innerWidth
  const winHeight = window.innerHeight

  const selectedWidth = winWidth / zoomsize
  const selectedHeight = winHeight / zoomsize

  const startX =  (event.clientX - (selectedWidth / 2)) / winWidth
  const endX = (event.clientX + (selectedWidth / 2)) / winWidth
  const startY = (event.clientY - (selectedHeight / 2)) / winHeight
  const endY = (event.clientY + (selectedHeight / 2)) / winHeight

  real = {
    from: ((real.to - real.from) * startX) + real.from,
    to: ((real.to - real.from) * endX) + real.from,
  }

  imag = {
    from: ((imag.to - imag.from) * startY) + imag.from,
    to: ((imag.to - imag.from) * endY) + imag.from,
  }

  drawMandelbrotSet(real.from, real.to, imag.from, imag.to)
})

drawMandelbrotSet(real.from, real.to, imag.from, imag.to)

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.