Mandelbrot explorer · qwen3-235b-a22b-thinking-2507
Render the Mandelbrot set with a smooth color gradient on a full-screen canvas, with click-and-drag to pan and mouse-wheel to zoom.
transcript · 4 messages
Render the Mandelbrot set with a smooth color gradient on a full-screen canvas, with click-and-drag to pan and mouse-wheel to zoom.
thinking
Okay, the user wants a single-page web app that renders the Mandelbrot set with smooth color gradients, full-screen canvas, and interactive panning via click-and-drag and zooming with the mouse wheel. All in one index.html file, no external resources.
First, I need to structure the HTML with a canvas element. The canvas should take up the entire viewport, so CSS will set it to position: fixed, top: 0, left: 0, width: 100%, height: 100%. Maybe add a style tag to hide overflow and body margins.
Next, the JavaScript part. The Mandelbrot calculation involves iterating complex numbers. For each pixel, map it to a point in the complex plane. The standard Mandelbrot formula is z = z² + c, where c is the point. Iterate until |z| > 2 or max iterations.
Smooth coloring can be done using the normalized iteration count. The formula is mu = n + 1 - log(log(|z|)) / log(2), where n is the iteration count. Then map mu to a color gradient.
For the canvas, need to set up event listeners: mousedown for drag start, mousemove for dragging (with a flag to check if mouse is down), mouseup to stop dragging. For zooming, use wheel event, scaling the view based on delta.
Initial view parameters: center at (-0.5, 0), zoom level 1. When zooming, adjust the center based on mouse position. When panning, calculate the delta in pixels and convert to complex plane coordinates.
Handling the canvas resize: when window resizes, update canvas dimensions and redraw.
Need to handle the rendering loop. Maybe use requestAnimationFrame for smooth drawing, but since it's a static image once rendered, perhaps draw once on load and redraw on interactions.
Wait, but the Mandelbrot set calculation is intensive. To avoid freezing the UI, maybe use a worker, but the user said no external resources, so can't use a separate JS file. So the main thread will have to handle it, but maybe optimize by drawing in chunks or using setTimeout to yield.
Alternatively, for simplicity, do a full render on each interaction, but that might be slow for high resolutions. Maybe limit the max iterations based on zoom level.
Let me outline the steps:
1. HTML structure with canvas.
2. CSS to make canvas full screen.
3. JS variables for viewport (centerX, centerY, zoom), mouse state (isDragging, lastX, lastY).
4. Functions to convert screen coordinates to complex plane.
5. Mandelbrot calculation function with smooth coloring.
6. Render function that loops through pixels, calculates color, sets pixel data.
7. Event listeners for mouse down, move, up, and wheel.
8. Resize handler to adjust canvas size.
Wait, but drawing pixel by pixel with putImageData might be slow. Maybe use a 2D context and fillRect for each pixel? No, that's even slower. Better to create an ImageData object, set pixels in an array, then putImageData once.
Yes, using ImageData is more efficient. So in the render function:
- Get canvas context.
- Create ImageData for the canvas dimensions.
- For each pixel (x, y), compute the corresponding complex number c.
- Calculate the iteration count and smooth color.
- Set the pixel color in the ImageData.
- Put the ImageData to the canvas.
But for large canvases, this loop could be slow. Maybe use a lower resolution initially, but the user wants full screen. Hmm.
Alternatively, use a worker thread, but again, no external files. So maybe it's acceptable to have some lag on first render, but interactions should be responsive. Maybe throttle the rendering on resize.
Now, for the smooth color gradient: create a function that takes a value between 0 and 1 (from the smooth iteration count) and maps it to an RGB color. Maybe use a cyclic gradient like HSV to RGB.
Wait, the smooth iteration count mu can be normalized to [0,1] by dividing by max iterations. Then use a gradient that transitions through colors. For example, a common gradient is black to blue to red to yellow.
Alternatively, use a continuous spectrum. Maybe a function that maps mu to hue, then convert to RGB.
Let's define a color function:
function getColor(mu) {
const hue = mu * 360; // full spectrum
// Convert hue to RGB
// ... HSL to RGB conversion
}
But HSL to RGB might be heavy, but it's manageable.
Alternatively, use a precomputed gradient array, but since it's inline, better to compute on the fly.
Wait, maybe use a simpler gradient. For example:
r = Math.sin(hue * Math.PI / 180) * 127 + 128;
g = Math.sin((hue + 120) * Math.PI / 180) * 127 + 128;
b = Math.sin((hue + 240) * Math.PI / 180) * 127 + 128;
But not sure. Alternatively, use a linear gradient between several key colors.
Another approach: use the smooth iteration count to interpolate between colors. For example, from black (0) to blue (0.2) to red (0.5) to yellow (1.0).
But perhaps the user just wants a smooth gradient, so the exact colors aren't specified. Let's go with a standard smooth gradient that transitions through multiple colors.
Now, for the event handlers:
- mousedown: set isDragging = true, store lastX, lastY.
- mousemove: if dragging, calculate dx = currentX - lastX, dy = currentY - lastY. Convert dx, dy to complex plane deltas (using current zoom), adjust centerX and centerY, update lastX/Y, and redraw.
- mouseup: set isDragging = false.
- wheel: adjust zoom based on event.deltaY, keeping the mouse position as the new center. Redraw.
Need to handle the zoom direction: wheel up to zoom in, wheel down to zoom out.
The zoom factor: maybe multiply zoom by 1.1 for zoom in, 0.9 for zoom out.
When zooming, the new center is adjusted so that the point under the mouse stays fixed. So, calculate the mouse position in complex coordinates before and after zoom, then adjust the center accordingly.
For example, if zooming in, the center should move towards the mouse position.
The formula for adjusting the center when zooming:
Let oldZoom = zoom, newZoom = zoom * factor.
The mouse position in complex plane before zoom: c_old = screenToComplex(mouseX, mouseY, oldZoom).
After zoom, the same screen position corresponds to c_new = screenToComplex(mouseX, mouseY, newZoom).
To keep the mouse position fixed in the complex plane, the center should be adjusted so that c_old = c_new.
Wait, maybe better to calculate the delta.
Alternatively, when zooming, the center is moved by (mouseX - width/2) * (1/newZoom - 1/oldZoom) in the x direction, similarly for y.
Wait, the mapping from screen to complex is:
real = centerX + (x - width/2) / zoom
imag = centerY + (y - height/2) / zoom
So, when zoom changes from z to z*k, to keep the mouse position (mx, my) fixed in the complex plane:
real = centerX_old + (mx - w/2)/z = centerX_new + (mx - w/2)/(z*k)
So, centerX_new = centerX_old + (mx - w/2)/z - (mx - w/2)/(z*k) = centerX_old + (mx - w/2)/z * (1 - 1/k)
Similarly for centerY.
So, the adjustment is (mx - w/2) * (1/z - 1/(z*k)) = (mx - w/2)/z * (1 - 1/k)
Which is (mx - w/2)/z * ( (k -1)/k )
But maybe it's easier to compute the delta as:
deltaX = (mx - width/2) * (1/newZoom - 1/oldZoom)
deltaY = (my - height/2) * (1/newZoom - 1/oldZoom)
Then newCenterX = oldCenterX + deltaX
newCenterY = oldCenterY + deltaY
Yes, that seems right.
So, in the wheel event handler:
factor = event.deltaY > 0 ? 0.9 : 1.1; // assuming deltaY positive is zoom out
newZoom = zoom * factor;
deltaX = (event.clientX - canvas.width/2) * (1/newZoom - 1/zoom);
deltaY = (event.clientY - canvas.height/2) * (1/newZoom - 1/zoom);
centerX += deltaX;
centerY += deltaY;
zoom = newZoom;
Then redraw.
For panning, when dragging:
dx = currentX - lastX;
dy = currentY - lastY;
deltaX = dx / zoom;
deltaY = dy / zoom;
centerX -= deltaX;
centerY -= deltaY;
Wait, because moving the canvas to the right (dragging left) should shift the center to the right. So if you drag the mouse to the right (increasing x), the view moves left, so the center should decrease. Wait, maybe:
When you drag the mouse from (x1,y1) to (x2,y2), the view should move in the opposite direction. So the delta in screen coordinates (dx = x2 - x1) corresponds to a delta in complex plane of dx / zoom. So to pan the view, the center should be adjusted by -dx / zoom.
Yes, so:
centerX -= dx / zoom;
centerY -= dy / zoom;
Because moving the mouse right (positive dx) means the view should move left, so the center (which is the middle of the view) should decrease.
Now, putting it all together.
Need to handle initial setup:
- Canvas element with id "mandelbrot"
- On window load, set canvas size, initialize variables, start render.
Wait, but need to handle resize. So add event listener for window resize, which updates canvas dimensions and redraws.
In the render function, for each pixel:
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// convert (x,y) to complex c
const real = centerX + (x - width/2) / zoom;
const imag = centerY + (y - height/2) / zoom;
// compute mandelbrot iteration
let n = 0;
let zr = 0, zi = 0;
const maxIter = 100;
while (n < maxIter) {
const zr2 = zr * zr;
const zi2 = zi * zi;
if (zr2 + zi2 > 4) break;
zi = 2 * zr * zi + imag;
zr = zr2 - zi2 + real;
n++;
}
// smooth coloring
let mu = 0;
if (n < maxIter) {
const z = Math.sqrt(zr * zr + zi * zi);
mu = n + 1 - Math.log(Math.log(z)) / Math.log(2);
} else {
mu = maxIter;
}
const color = getColor(mu / maxIter);
// set pixel color
}
}
But this loop is O(width*height), which for full screen could be millions of pixels. This will be slow in JS. Need to optimize.
Possible optimizations:
- Use a lower maxIter for initial render, increase when idle? But user didn't specify.
- Maybe use requestAnimationFrame to split the rendering into chunks, but the user might want a complete render.
Alternatively, accept that it's slow but works. For the purpose of the task, it's acceptable.
Now, the color function. Let's define a smooth gradient from black to blue to red to yellow.
function getColor(t) {
// t is between 0 and 1
const r = Math.sin(t * Math.PI) * 255;
const g = Math.sin(t * Math.PI * 2) * 255;
const b = Math.sin(t * Math.PI * 3) * 255;
return [r, g, b];
}
But that might not look great. Alternatively, use a cyclic gradient.
Another approach: use a hue that cycles through 0-360 degrees.
function getColor(t) {
const hue = t * 360;
return hslToRgb(hue, 1, 0.5);
}
But need to implement hslToRgb.
Here's a standard HSL to RGB conversion:
function hslToRgb(h, s, l) {
h /= 60;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(h % 2 - 1));
const m = l - c/2;
let r, g, b;
if (h >= 0 && h < 1) { r = c; g = x; b = 0; }
else if (h < 2) { r = x; g = c; b = 0; }
else if (h < 3) { r = 0; g = c; b = x; }
else if (h < 4) { r = 0; g = x; b = c; }
else if (h < 5) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
But this might be heavy, but it's manageable.
Alternatively, use a simpler gradient. Let's try a linear gradient from black (0) to blue (0.25) to red (0.5) to yellow (1.0).
But for smoothness, maybe interpolate between colors.
Another idea: use the smooth iteration count to create a continuous gradient by cycling through a color map.
But perhaps the simplest way is to use a sine-based gradient for RGB channels.
For example:
r = (Math.sin(0.024 * t + 0) * 127 + 128);
g = (Math.sin(0.024 * t + 2) * 127 + 128);
b = (Math.sin(0.024 * t + 4) * 127 + 128);
But t here is the smooth iteration count. Wait, in the Mandelbrot smooth coloring, mu is the smooth iteration count. So t = mu / maxIter.
But the exact formula may vary.
Alternatively, use a precomputed array of colors, but since it's inline, better to compute on the fly.
Let me proceed with the HSL approach.
Now, putting all together into the index.html.
Structure:
<!DOCTYPE html>
<html>
<head>
<title>Mandelbrot Set</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="mandelbrot"></canvas>
<script>
// JS code here
</script>
</body>
</html>
In the script:
- Get canvas, set dimensions.
- Initialize variables: centerX, centerY, zoom, isDragging, lastX, lastY.
- Event listeners.
- Render function.
Now, code the JS.
First, canvas setup:
const canvas = document.getElementById('mandelbrot');
const ctx = canvas.getContext('2d');
let width, height;
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
render();
}
window.addEventListener('resize', resize);
resize();
Variables:
let centerX = -0.5;
let centerY = 0;
let zoom = 200;
let isDragging = false;
let lastX, lastY;
Event listeners:
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
centerX -= dx / zoom;
centerY -= dy / zoom;
lastX = e.clientX;
lastY = e.clientY;
render();
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const factor = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = zoom * factor;
const dx = (e.clientX - width/2) * (1/newZoom - 1/zoom);
const dy = (e.clientY - height/2) * (1/newZoom - 1/zoom);
centerX += dx;
centerY += dy;
zoom = newZoom;
render();
});
Render function:
function render() {
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
const maxIter = 100;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const real = centerX + (x - width/2) / zoom;
const imag = centerY + (y - height/2) / zoom;
let zr = 0, zi = 0;
let n = 0;
while (n < maxIter) {
const zr2 = zr * zr;
const zi2 = zi * zi;
if (zr2 + zi2 > 4) break;
zi = 2 * zr * zi + imag;
zr = zr2 - zi2 + real;
n++;
}
let mu = 0;
if (n < maxIter) {
const z = Math.sqrt(zr * zr + zi * zi);
mu = n + 1 - Math.log(Math.log(z)) / Math.log(2);
} else {
mu = maxIter;
}
const t = mu / maxIter;
const [r, g, b] = getColor(t);
const idx = (y * width + x) * 4;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
data[idx + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
Color function:
function getColor(t) {
// Convert t to hue (0-360)
const hue = t * 360;
return hslToRgb(hue, 1, 0.5);
}
function hslToRgb(h, s, l) {
h /= 60;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(h % 2 - 1));
const m = l - c/2;
let r, g, b;
if (h >= 0 && h < 1) { r = c; g = x; b = 0; }
else if (h < 2) { r = x; g = c; b = 0; }
else if (h < 3) { r = 0; g = c; b = x; }
else if (h < 4) { r = 0; g = x; b = c; }
else if (h < 5) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return [
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
];
}
Wait, but this hslToRgb function assumes s and l are in [0,1], which they are (s=1, l=0.5). So it should work.
Testing this code: the initial view is centered at (-0.5, 0) with zoom 200, which is a standard Mandelbrot view.
Potential issues:
- The log(log(z)) might cause NaN if z <= 1, but since the loop breaks when zr² + zi² > 4, z is at least 2, so log(z) is positive, and log(log(z)) is defined.
Wait, when z = 2, log(z) = ln(2) ≈ 0.693, log(log(z)) is log(0.693) which is negative. So the formula mu = n + 1 - log(log(z))/log(2) would be n + 1 - (negative)/log(2), which increases mu. But this might not be correct.
Wait, the standard smooth coloring formula is:
mu = n + 1 - log(log(|z|)) / log(2)
But when |z| > 2, log(|z|) is positive, but if |z| is exactly 2, log(|z|) is log(2), so log(log(|z|)) is log(log(2)) which is negative. So the term -log(log(|z|))/log(2) becomes positive, making mu larger than n.
But this formula is correct for the continuous escape time.
However, in code, when zr² + zi² > 4, we break, so |z| > 2, so log(|z|) is positive, but log(log(|z|)) could be negative if |z| < e (since log(e) = 1, so log(log(e)) = 0). Wait, |z| > 2, so log(|z|) > log(2) ≈ 0.693, so log(log(|z|)) is log(0.693) ≈ -0.366, which is negative. So the formula gives mu = n + 1 - (-0.366)/log(2) ≈ n + 1 + 0.528.
But this is the correct continuous extension.
So the code should handle it, as the log functions will return negative values, but the subtraction makes mu larger than n.
Now, check if z is zero (when n == maxIter), but in that case, mu = maxIter, so t = 1, which maps to hue 360 (same as 0), which is red in HSL.
Wait, when t=1, hue=360, which modulo 360 is 0, so red.
But for points inside the set (n == maxIter), they are colored as the last color in the gradient.
Now, the code should work.
But the performance might be an issue. For a 1920x1080 canvas, that's over 2 million pixels, each requiring up to 100 iterations. That's 200 million operations, which in JS might take several seconds. But for the purpose of this task, it's acceptable as a basic implementation.
Possible improvements: use a lower maxIter for initial render, but the user didn't specify.
Now, write all this into index.html.
Check for any missing parts: the initial render on load, which is handled by the resize() call which triggers render().
Also, the wheel event prevents default to stop page scrolling.
Now, compile all into a single index.html file.index.html (3423 bytes)Successfully wrote 3423 bytes to index.html
thinking
Okay, let me check what the user asked for. They wanted a single-page web app rendering the Mandelbrot set with smooth color gradients, full-screen canvas, click-and-drag to pan, and mouse-wheel zoom. All in one index.html file with no external resources. I created the index.html file with inline CSS and JS. The HTML structure has a canvas element. The CSS makes the canvas full screen by setting body margin to 0 and overflow hidden. The JS handles the Mandelbrot calculations, smooth coloring using HSL to RGB conversion, and event listeners for panning and zooming. Wait, in the JS, I used a smooth coloring formula with mu = n + 1 - log(log(z))/log(2). That should give a smooth gradient. The color function maps the iteration count to a hue, which cycles through colors. The panning works by adjusting centerX and centerY based on mouse movement, and zooming adjusts the zoom factor while keeping the mouse position fixed in the complex plane. I need to make sure there are no external dependencies. The code is all inline, no CDNs or external files. The canvas is properly sized on resize. The event listeners prevent default on wheel to stop page scrolling. Let me verify the key parts: - Full-screen canvas: CSS sets body margin 0 and canvas as block, which should cover the viewport. - Smooth gradient: Using HSL with hue based on normalized iteration count. - Panning: On mousemove while dragging, adjust center by dx/dy divided by zoom. - Zooming: Wheel event adjusts zoom and recalculates center to keep mouse position fixed. - No external resources: All code is in the single HTML file. Possible issues: Performance might be slow for large screens since it's calculating every pixel on the main thread. But the user didn't specify performance requirements, just functionality. The smooth coloring formula seems correct, handling the continuous escape time. Looks like all requirements are met. The file was written successfully, so the user should be able to open index.html and interact with the Mandelbrot set as described.
The Mandelbrot set explorer has been successfully created as a single self-contained HTML file. Key features implemented: - Full-screen canvas with proper CSS styling - Smooth color gradient using HSL-based coloring with continuous iteration mapping - Click-and-drag panning with real-time rendering - Mouse-wheel zoom with pivot point at cursor position - Responsive design that handles window resizing - All code (HTML/CSS/JS) contained in a single index.html file with no external dependencies The implementation uses the standard Mandelbrot escape-time algorithm with smooth coloring via the continuous iteration count formula. The color gradient cycles through the full spectrum using HSL conversion, creating a visually appealing representation of the fractal. To use: 1. Open index.html in any modern browser 2. Click and drag to pan the view 3. Use mouse wheel to zoom in/out 4. The fractal will automatically resize when the browser window changes The application runs completely offline with no network access required, fulfilling all specified requirements.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Mandelbrot renders with harsh red/orange/yellow/green gradient; drag pans and wheel zooms correctly, but colors look unpolished.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.