For lack of a better place, let me include it here: an elegant implementation of the Mandelbrot set, created in part using assistance from the AI (Claude).
Yes, you can pan and zoom.
And the source code:
<!DOCTYPE html>
<html>
<head>
<title>Mandelbrot Set</title>
<style>
canvas
{
border: 1px solid black;
}
.tooltip
{
position: absolute;
background-color: rgba(255, 240, 230, 0.8);
color: black;
padding: 2px;
border-radius: 3px;
border: 1px solid #333;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
font-family: monospace;
}
</style>
</head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<div id="tooltip" class="tooltip"></div>
<script>
function doWork(event)
{
function hslToRgb(h, s, l)
{
let r, g, b;
if (s === 0)
{
r = g = b = l;
}
else
{
const hue2rgb = (p, q, t) =>
{
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
const { width, height, zoom, offsetX, offsetY } = event.data;
const size = Math.min(width, height);
const maxIterations = 100;
const pixels = new Uint8ClampedArray(width * height * 4);
for (let x = 0; x < width; x++)
{
for (let y = 0; y < height; y++)
{
let zx = 0;
let zy = 0;
let cx = (x - width / 2) / (0.5 * zoom * size) + offsetX;
let cy = (y - height / 2) / (0.5 * zoom * size) + offsetY;
let i = 0;
while (zx * zx + zy * zy < 4 && i < maxIterations)
{
const tempX = zx * zx - zy * zy + cx;
zy = 2 * zx * zy + cy;
zx = tempX;
i++;
}
const index = (y * width + x) * 4;
if (i === maxIterations)
{
pixels[index] = 0;
pixels[index + 1] = 0;
pixels[index + 2] = 0;
}
else
{
const hue = i * 5;
const rgb = hslToRgb(hue / 360, 1, 0.5);
pixels[index] = rgb[0];
pixels[index + 1] = rgb[1];
pixels[index + 2] = rgb[2];
}
pixels[index + 3] = 255;
}
}
postMessage({ pixels: pixels }, [pixels.buffer]);
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let zoom = 1;
let offsetX = -0.5;
let offsetY = 0;
let isDragging = false;
let lastX = 0;
let lastY = 0;
let redrawTimeout = null;
const worker = new Worker(window.URL.createObjectURL(
new Blob(["onmessage=" + doWork.toString()], {type: "text/javascript"})));
worker.onmessage = (event) =>
{
const imageData = new ImageData(event.data.pixels, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
document.body.style.cursor = 'default';
};
function drawMandelbrot()
{
document.body.style.cursor = 'wait';
worker.postMessage({
width: canvas.width,
height: canvas.height,
zoom: zoom,
offsetX: offsetX,
offsetY: offsetY,
});
}
canvas.addEventListener('wheel', (event) =>
{
event.preventDefault();
const tooltip = document.getElementById('tooltip');
tooltip.style.opacity = 0;
const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9;
const mouseX = event.offsetX;
const mouseY = event.offsetY;
const size = Math.min(canvas.width, canvas.height);
const dx = (mouseX - canvas.width / 2) / (0.5 * size * zoom);
const dy = (mouseY - canvas.height / 2) / (0.5 * size * zoom);
zoom *= zoomFactor;
offsetX -= dx - dx * zoomFactor;
offsetY -= dy - dy * zoomFactor;
drawMandelbrot();
});
canvas.addEventListener('mousedown', (event) =>
{
isDragging = true;
lastX = event.offsetX;
lastY = event.offsetY;
});
let hoverTimeout = null;
canvas.addEventListener('mousemove', (event) =>
{
const tooltip = document.getElementById('tooltip');
tooltip.style.opacity = 0;
const size = Math.min(canvas.width, canvas.height);
if (isDragging)
{
const dx = (event.offsetX - lastX) / (0.5 * size * zoom);
const dy = (event.offsetY - lastY) / (0.5 * size * zoom);
offsetX -= dx;
offsetY -= dy;
lastX = event.offsetX;
lastY = event.offsetY;
// Clear the previous timeout
if (redrawTimeout)
{
clearTimeout(redrawTimeout);
}
// Set a new timeout to redraw after a short delay
redrawTimeout = setTimeout(() =>
{
drawMandelbrot();
}, 16); // Adjust the delay as needed (e.g., 16ms for 60 FPS)
}
else
{
const x = (event.offsetX - canvas.width / 2) / (0.5 * zoom * size) + offsetX;
const y = (event.offsetY - canvas.height / 2) / (0.5 * zoom * size) + offsetY;
// Clear the previous timeout
if (hoverTimeout)
{
clearTimeout(hoverTimeout);
}
function formatFloat(x)
{
if (x == 0 || (Math.abs(x) >= 0.1 && Math.abs(x) < 10000)) return x.toFixed(4);
else return x.toExponential(3);
}
// Set a new timeout to show the tooltip after 1 second
hoverTimeout = setTimeout(() =>
{
tooltip.textContent =
`x:${formatFloat(x)}, y:${-formatFloat(y)}, zoom:${formatFloat(zoom)}`; tooltip.style.left = `${event.pageX + 10}px`;
tooltip.style.top = `${event.pageY + 10}px`;
tooltip.style.opacity = 1;
}, 1000);
}
});
canvas.addEventListener('mouseout', () =>
{
// Clear the timeout and hide the tooltip when the mouse leaves the canvas
if (hoverTimeout)
{
clearTimeout(hoverTimeout);
}
const tooltip = document.getElementById('tooltip');
tooltip.style.opacity = 0;
});
canvas.addEventListener('mouseup', () =>
{
isDragging = false;
if (redrawTimeout)
{
clearTimeout(redrawTimeout);
drawMandelbrot();
}
});
canvas.addEventListener('mouseleave', () =>
{
isDragging = false;
if (redrawTimeout)
{
clearTimeout(redrawTimeout);
drawMandelbrot();
}
});
window.addEventListener('keydown', function(e)
{
if (e.ctrlKey && e.key === '0')
{
zoom = 1;
offsetX = -0.5;
offsetY = 0;
drawMandelbrot();
}
});
drawMandelbrot();
</script>
</body>
</html>