Zoomable Mandelbrot

import { fmandelbrot } from "./components/mandelbrot.js";

const MAX_ITER = 512;

const W = 800;
const H = 800;

const state = {
  xscale: d3.scaleLinear([0, W], [-2, 0.6]),
  yscale: d3.scaleLinear([0, H], [1.25, -1.25]),
  coords: [0, 0],
  infoEnabled: false,
};

const colors = new Map();
const colorscale = d3
  .scaleSequentialLog(d3.interpolateInferno)
  .domain([1, MAX_ITER]);
for (let i = 1; i < MAX_ITER; i++) {
  colors.set(i, colorscale(i));
}
colors.set(MAX_ITER, `rgb(0, 0, 0)`);

const ctx = document.querySelector("canvas#mcanv").getContext("2d");
const drawRow = (ctx, xscale, yscale, y = 0, rows = 20) => {
  for (let i = 0; i < rows; i++) {
    for (let x = 0; x <= W; x++) {
      const l = fmandelbrot([xscale(x), yscale(y + i)], MAX_ITER);
      ctx.fillStyle = colors.get(l);
      ctx.fillRect(x, y + i, 1, 1);
    }
  }
  if (y < H) {
    requestAnimationFrame(() => drawRow(ctx, xscale, yscale, y + rows, rows));
  }
};
requestAnimationFrame(() => drawRow(ctx, state.xscale, state.yscale));

const ocanvas = document.querySelector("canvas#overlay");
const overlay = ocanvas.getContext("2d");
const bbox = ocanvas.getBoundingClientRect();

overlay.font = "24px Arial";
let mousedown = false;

const drawDebug = () => {
  overlay.fillStyle = "white";
  overlay.strokeStyle = "black";
  overlay.lineWidth = 2;
  overlay.strokeText(`x scale: ${state.xscale.range()}`, 10, 30);
  overlay.fillText(`x scale: ${state.xscale.range()}`, 10, 30);
  overlay.strokeText(`y scale: ${state.yscale.range()}`, 10, 60);
  overlay.fillText(`y scale: ${state.yscale.range()}`, 10, 60);
};

const clearOverlay = () => {
  overlay.clearRect(0, 0, W, H);
};

document.querySelector("#overlay").addEventListener("mousedown", (e) => {
  const mx = e.clientX - bbox.left;
  const my = e.clientY - bbox.top;
  state.coords = [mx, my];
  mousedown = true;
});

document.querySelector("#overlay").addEventListener("mousemove", (e) => {
  if (mousedown) {
    const mx = e.clientX - bbox.left;
    const my = e.clientY - bbox.top;
    clearOverlay();
    overlay.strokeStyle = "white";
    overlay.strokeRect(
      state.coords[0],
      state.coords[1],
      mx - state.coords[0],
      my - state.coords[1],
    );
    console.log(state.infoEnabled);
    if (state.infoEnabled) drawDebug();
  }
});

document.querySelector("#overlay").addEventListener("mouseup", (e) => {
  if (!mousedown) {
    return;
  }
  mousedown = false;
  clearOverlay();
  const mx = e.clientX - bbox.left;
  const my = e.clientY - bbox.top;
  const minx = Math.min(state.coords[0], mx);
  const maxx = Math.max(state.coords[0], mx);
  const miny = Math.min(state.coords[1], my);
  const maxy = Math.max(state.coords[1], my);
  const maxDiff = Math.max(maxx - minx, maxy - miny);
  state.xscale = d3.scaleLinear(
    [0, W],
    [state.xscale(minx), state.xscale(minx + maxDiff)],
  );
  state.yscale = d3.scaleLinear(
    [0, H],
    [state.yscale(miny), state.yscale(miny + maxDiff)],
  );
  if (state.infoEnabled) drawDebug();
  requestAnimationFrame(() => drawRow(ctx, state.xscale, state.yscale));
});

document.addEventListener("keydown", (event) => {
  // Check if the "i" key was pressed
  if (event.key === "i") {
    if (!state.infoEnabled) {
      state.infoEnabled = true;
      drawDebug();
    } else {
      state.infoEnabled = false;
      clearOverlay();
    }
  }
});