Prompt
Build an interactive Mandelbrot explorer in Three.js with pan-and-zoom controls and a smooth deep-red-to-yellow gradient mapped to escape-time iterations.
A system prompt was added to support web rendering
Of course. Here is a complete, self-contained interactive Mandelbrot set explorer built with Three.js. It features smooth pan-and-zoom controls and a vibrant deep-red-to-yellow color gradient for the escape-time iterations. Simply copy the code below into an `.html` file and open it in your web browser. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Mandelbrot Explorer</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } canvas { display: block; } </style> </head> <body> <!-- Three.js library import via importmap --> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script> <!-- Vertex Shader: Positions the plane --> <script type="x-shader/x-vertex" id="vertexShader"> varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } </script> <!-- Fragment Shader: Calculates and Colors the Mandelbrot Set --> <script type="x-shader/x-fragment" id="fragmentShader"> varying vec2 vUv; uniform vec2 u_resolution; uniform vec2 u_offset; uniform float u_zoom; uniform int u_max_iterations; // Function to map the smooth iteration count to a color gradient vec3 color_gradient(float t) { vec3 deep_red = vec3(0.5, 0.0, 0.0); vec3 red = vec3(1.0, 0.1, 0.0); vec3 yellow = vec3(1.0, 1.0, 0.0); // Create a smooth transition from deep red -> red -> yellow t = pow(t, 0.5); // Use pow to stretch the gradient for more detail vec3 color = mix(deep_red, red, smoothstep(0.0, 0.4, t)); color = mix(color, yellow, smoothstep(0.4, 0.8, t)); return color; } void main() { // Adjust UV coordinates for aspect ratio to prevent stretching vec2 aspect_ratio = vec2(u_resolution.x / u_resolution.y, 1.0); // Map the fragment's screen coordinates to a point in the complex plane // based on the current offset and zoom level. vec2 c = (vUv - 0.5) * 2.0 * aspect_ratio * u_zoom + u_offset; vec2 z = vec2(0.0); int iterations = 0; // The main Mandelbrot iteration loop for (int i = 0; i < u_max_iterations; i++) { // z = z^2 + c // (zx + zy*i)^2 = zx^2 - zy^2 + 2*zx*zy*i z = vec2( z.x * z.x - z.y * z.y, 2.0 * z.x * z.y ) + c; iterations = i; // If |z| > 2, the point has escaped. // We check |z|^2 > 4 to avoid a costly sqrt() operation. if (dot(z, z) > 4.0) { break; } } // If the point never escaped, it's inside the set. Color it black. if (iterations == u_max_iterations - 1) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { // For points that escaped, calculate a "smooth" iteration count for a continuous gradient. // This formula avoids the banding seen when coloring by integer iteration counts. float smooth_iter = float(iterations) + 1.0 - log(log(length(z))) / log(2.0); // Normalize the smooth iteration count to a 0.0 - 1.0 range float t = smooth_iter / float(u_max_iterations); // Map the normalized value to our color gradient vec3 color = color_gradient(t); gl_FragColor = vec4(color, 1.0); } } </script> <!-- Main Application Logic --> <script type="module"> import * as THREE from 'three'; // SCENE SETUP const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // SHADER UNIFORMS (variables passed to the shader) const uniforms = { u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_zoom: { value: 2.0 }, // Initial zoom level u_offset: { value: new THREE.Vector2(-0.5, 0.0) }, // Initial center point u_max_iterations: { value: 200 } }; // GEOMETRY & MATERIAL const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: document.getElementById('vertexShader').textContent, fragmentShader: document.getElementById('fragmentShader').textContent }); const plane = new THREE.Mesh(geometry, material); scene.add(plane); camera.position.z = 1; // INTERACTIVITY CONTROLS let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; const canvas = renderer.domElement; // Panning (Mouse Drag) canvas.addEventListener('mousedown', (e) => { isDragging = true; previousMousePosition.x = e.clientX; previousMousePosition.y = e.clientY; }); canvas.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - previousMousePosition.x; const dy = e.clientY - previousMousePosition.y; // Convert pixel delta to complex plane delta const aspect = uniforms.u_resolution.value.x / uniforms.u_resolution.value.y; uniforms.u_offset.value.x -= dx / window.innerWidth * 2.0 * uniforms.u_zoom.value * aspect; uniforms.u_offset.value.y += dy / window.innerHeight * 2.0 * uniforms.u_zoom.value; previousMousePosition.x = e.clientX; previousMousePosition.y = e.clientY; }); canvas.addEventListener('mouseup', () => { isDragging = false; }); canvas.addEventListener('mouseleave', () => { isDragging = false; }); // Stop dragging if mouse leaves canvas // Zooming (Mouse Wheel) canvas.addEventListener('wheel', (e) => { e.preventDefault(); const zoomFactor = 0.9; const oldZoom = uniforms.u_zoom.value; // Determine zoom direction const newZoom = e.deltaY > 0 ? oldZoom / zoomFactor : oldZoom * zoomFactor; // Get mouse position in normalized device coordinates (-1 to +1) const mouseX = (e.clientX / window.innerWidth) * 2 - 1; const mouseY = -(e.clientY / window.innerHeight) * 2 + 1; // Calculate mouse position in the complex plane before zoom const aspect = uniforms.u_resolution.value.x / uniforms.u_resolution.value.y; const mouseCoordBefore = new THREE.Vector2( (mouseX * aspect * oldZoom) + uniforms.u_offset.value.x, (mouseY * oldZoom) + uniforms.u_offset.value.y ); // Apply zoom uniforms.u_zoom.value = newZoom; // Recalculate mouse position after zoom (with the old offset) const mouseCoordAfter = new THREE.Vector2( (mouseX * aspect * newZoom) + uniforms.u_offset.value.x, (mouseY * newZoom) + uniforms.u_offset.value.y ); // Adjust the offset to keep the point under the cursor stationary uniforms.u_offset.value.x += mouseCoordBefore.x - mouseCoordAfter.x; uniforms.u_offset.value.y += mouseCoordBefore.y - mouseCoordAfter.y; // Increase iterations on deep zooms for more detail if(uniforms.u_zoom.value < 0.001) { uniforms.u_max_iterations.value = 500; } else if (uniforms.u_zoom.value < 0.01) { uniforms.u_max_iterations.value = 350; } else { uniforms.u_max_iterations.value = 200; } }); // WINDOW RESIZE HANDLING window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.x = window.innerWidth; uniforms.u_resolution.value.y = window.innerHeight; }); // RENDER LOOP function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Mandelbrot Explorer</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1; const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const geometry = new THREE.PlaneGeometry(2, 2); const uniforms = { center: { value: new THREE.Vector2(0, 0) }, zoom: { value: 1.0 }, maxIterations: { value: 100 }, aspectRatio: { value: window.innerWidth / window.innerHeight } }; const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragmentShader = ` uniform vec2 center; uniform float zoom; uniform int maxIterations; uniform float aspectRatio; varying vec2 vUv; void main() { vec2 uv = vUv; float real = center.x + (uv.x - 0.5) * 4.0 / zoom * aspectRatio; float imag = center.y + (uv.y - 0.5) * 4.0 / zoom; vec2 z = vec2(0.0); vec2 c = vec2(real, imag); int i; for (i = 0; i < maxIterations; i++) { float zr = z.x * z.x - z.y * z.y + c.x; float zi = 2.0 * z.x * z.y + c.y; z = vec2(zr, zi); if (dot(z, z) > 4.0) break; } if (i == maxIterations) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { float smoothed = float(i) - log(log(length(z))) / log(2.0); float t = smoothed / float(maxIterations); vec3 color = mix(vec3(1.0, 0.0, 0.0), vec3(1.0, 1.0, 0.0), t); gl_FragColor = vec4(color, 1.0); } } `; const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vertexShader, fragmentShader: fragmentShader }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Pan controls let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; renderer.domElement.addEventListener('mousedown', (e) => { isDragging = true; previousMousePosition = { x: e.clientX, y: e.clientY }; }); renderer.domElement.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - previousMousePosition.x; const deltaY = e.clientY - previousMousePosition.y; const width = renderer.domElement.width; const height = renderer.domElement.height; const aspectRatio = width / height; const zoom = uniforms.zoom.value; const deltaReal = (deltaX / width) * (4.0 / zoom) * aspectRatio; const deltaImag = (deltaY / height) * (4.0 / zoom); uniforms.center.value.x -= deltaReal; uniforms.center.value.y += deltaImag; // Y is inverted in screen coordinates previousMousePosition = { x: e.clientX, y: e.clientY }; }); renderer.domElement.addEventListener('mouseup', () => { isDragging = false; }); renderer.domElement.addEventListener('mouseleave', () => { isDragging = false; }); // Zoom controls renderer.domElement.addEventListener('wheel', (e) => { e.preventDefault(); const zoomFactor = 1.1; const delta = e.deltaY > 0 ? 1 / zoomFactor : zoomFactor; const width = renderer.domElement.width; const height = renderer.domElement.height; const aspectRatio = width / height; const factor = 4.0; const zoom = uniforms.zoom.value; const newZoom = zoom * delta; const x = e.clientX; const y = e.clientY; // Current real and imag at mouse position const real = uniforms.center.value.x + (x / width - 0.5) * (factor / zoom) * aspectRatio; const imag = uniforms.center.value.y + (0.5 - y / height) * (factor / zoom); // Update center uniforms.center.value.x = real - (x / width - 0.5) * (factor / newZoom) * aspectRatio; uniforms.center.value.y = imag - (0.5 - y / height) * (factor / newZoom); uniforms.zoom.value = newZoom; }); // Handle window resize window.addEventListener('resize', () => { const width = window.innerWidth; const height = window.innerHeight; renderer.setSize(width, height); uniforms.aspectRatio.value = width / height; }); // Animation loop function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Mandelbrot Explorer</title> <style>body { margin: 0; }</style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> <script> const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: { center: { value: new THREE.Vector2(-0.5, 0) }, scale: { value: 3.0 }, aspect: { value: window.innerWidth / window.innerHeight }, maxIter: { value: 256 } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 center; uniform float scale; uniform float aspect; uniform int maxIter; varying vec2 vUv; void main() { vec2 p = (vUv - 0.5) * vec2(scale * aspect, scale); p += center; vec2 z = vec2(0.0); vec2 c = p; int iter = 0; float sq = 0.0; while (iter < maxIter) { z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; sq = z.x * z.x + z.y * z.y; if (sq > 4.0) break; iter++; } if (iter < maxIter) { float mu = float(iter) - log(log(sqrt(sq)) / log(2.0)) / log(2.0); float t = mu / float(maxIter); vec3 color = mix(vec3(0.2, 0.0, 0.0), vec3(1.0, 1.0, 0.0), t); gl_FragColor = vec4(color, 1.0); } else { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } } ` }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); // Resize handler window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); material.uniforms.aspect.value = window.innerWidth / window.innerHeight; }); // Pan controls let dragging = false; let lastX, lastY; renderer.domElement.addEventListener('mousedown', (e) => { dragging = true; lastX = e.clientX; lastY = e.clientY; }); renderer.domElement.addEventListener('mousemove', (e) => { if (dragging) { const dx = e.clientX - lastX; const dy = e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; const aspect = window.innerWidth / window.innerHeight; const full_width = material.uniforms.scale.value * aspect; const full_height = material.uniforms.scale.value; const pixel_size_x = full_width / window.innerWidth; const pixel_size_y = full_height / window.innerHeight; material.uniforms.center.value.x += -dx * pixel_size_x; material.uniforms.center.value.y += dy * pixel_size_y; } }); document.addEventListener('mouseup', () => { dragging = false; }); // Zoom controls renderer.domElement.addEventListener('wheel', (e) => { e.preventDefault(); const uvx = e.clientX / window.innerWidth; const uvy = 1 - e.clientY / window.innerHeight; const aspect = window.innerWidth / window.innerHeight; const old_scale = material.uniforms.scale.value; const factor = e.deltaY < 0 ? 0.9 : 1.1; const new_scale = old_scale * factor; const old_scl_x = old_scale * aspect; const old_scl_y = old_scale; const new_scl_x = new_scale * aspect; const new_scl_y = new_scale; const duv_x = uvx - 0.5; const duv_y = uvy - 0.5; const delta_x = duv_x * (old_scl_x - new_scl_x); const delta_y = duv_y * (old_scl_y - new_scl_y); material.uniforms.center.value.x += delta_x; material.uniforms.center.value.y += delta_y; material.uniforms.scale.value = new_scale; }, { passive: false }); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Mandelbrot Explorer</title> <style> body { margin: 0; padding: 0; overflow: hidden; background: black; font-family: Arial, sans-serif; } #container { position: relative; width: 100vw; height: 100vh; } #controls { position: absolute; top: 10px; left: 10px; z-index: 100; color: white; background: rgba(0, 0, 0, 0.7); padding: 15px; border-radius: 8px; font-size: 14px; } #info { position: absolute; bottom: 10px; left: 10px; z-index: 100; color: white; background: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 8px; font-size: 12px; } button { background: #444; color: white; border: none; padding: 5px 10px; margin: 2px; border-radius: 3px; cursor: pointer; } button:hover { background: #666; } </style> </head> <body> <div id="container"> <div id="controls"> <div>Mandelbrot Explorer</div> <div style="margin-top: 10px;"> <button onclick="resetView()">Reset View</button> <button onclick="increaseIterations()">More Detail</button> <button onclick="decreaseIterations()">Less Detail</button> </div> <div style="margin-top: 8px;"> Iterations: <span id="iterCount">100</span> </div> </div> <div id="info"> Left click and drag to pan<br> Scroll or right click to zoom<br> Double click to zoom in on point </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> let scene, camera, renderer, plane, material; let mouse = { x: 0, y: 0 }; let isMouseDown = false; let lastMouse = { x: 0, y: 0 }; // Mandelbrot parameters let center = { x: -0.5, y: 0.0 }; let zoom = 1.0; let maxIterations = 100; // Shader for Mandelbrot calculation const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragmentShader = ` uniform vec2 u_center; uniform float u_zoom; uniform float u_maxIterations; uniform vec2 u_resolution; varying vec2 vUv; vec3 getColor(float iterations) { if (iterations >= u_maxIterations) { return vec3(0.0, 0.0, 0.0); } float t = iterations / u_maxIterations; // Smooth deep-red to yellow gradient vec3 color1 = vec3(0.5, 0.0, 0.0); // Deep red vec3 color2 = vec3(0.8, 0.1, 0.0); // Red-orange vec3 color3 = vec3(1.0, 0.5, 0.0); // Orange vec3 color4 = vec3(1.0, 0.8, 0.0); // Yellow-orange vec3 color5 = vec3(1.0, 1.0, 0.2); // Bright yellow if (t < 0.25) { return mix(color1, color2, t * 4.0); } else if (t < 0.5) { return mix(color2, color3, (t - 0.25) * 4.0); } else if (t < 0.75) { return mix(color3, color4, (t - 0.5) * 4.0); } else { return mix(color4, color5, (t - 0.75) * 4.0); } } void main() { vec2 coord = vUv * 2.0 - 1.0; coord.x *= u_resolution.x / u_resolution.y; vec2 c = u_center + coord / u_zoom; vec2 z = vec2(0.0); float iterations = 0.0; for (int i = 0; i < 1000; i++) { if (iterations >= u_maxIterations) break; if (dot(z, z) > 4.0) break; z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; iterations += 1.0; } // Smooth iteration count for better gradients if (iterations < u_maxIterations) { float zn = dot(z, z); iterations = iterations + 1.0 - log(log(zn) / log(2.0)) / log(2.0); } vec3 color = getColor(iterations); gl_FragColor = vec4(color, 1.0); } `; function init() { // Scene setup scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1); renderer = new THREE.WebGLRenderer({ antialias: false }); renderer.setSize(window.innerWidth, window.innerHeight); document.getElementById('container').appendChild(renderer.domElement); // Create plane geometry const geometry = new THREE.PlaneGeometry(2, 2); // Create shader material material = new THREE.ShaderMaterial({ uniforms: { u_center: { value: new THREE.Vector2(center.x, center.y) }, u_zoom: { value: zoom }, u_maxIterations: { value: maxIterations }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) } }, vertexShader: vertexShader, fragmentShader: fragmentShader }); plane = new THREE.Mesh(geometry, material); scene.add(plane); // Event listeners setupEventListeners(); // Start render loop animate(); } function setupEventListeners() { const canvas = renderer.domElement; // Mouse events canvas.addEventListener('mousedown', onMouseDown, false); canvas.addEventListener('mousemove', onMouseMove, false); canvas.addEventListener('mouseup', onMouseUp, false); canvas.addEventListener('wheel', onWheel, false); canvas.addEventListener('dblclick', onDoubleClick, false); canvas.addEventListener('contextmenu', (e) => e.preventDefault(), false); // Touch events for mobile canvas.addEventListener('touchstart', onTouchStart, false); canvas.addEventListener('touchmove', onTouchMove, false); canvas.addEventListener('touchend', onTouchEnd, false); // Window resize window.addEventListener('resize', onWindowResize, false); } function onMouseDown(event) { isMouseDown = true; lastMouse.x = event.clientX; lastMouse.y = event.clientY; } function onMouseMove(event) { if (isMouseDown) { const deltaX = (event.clientX - lastMouse.x) / window.innerWidth; const deltaY = (event.clientY - lastMouse.y) / window.innerHeight; const aspectRatio = window.innerWidth / window.innerHeight; center.x -= deltaX * 2.0 * aspectRatio / zoom; center.y += deltaY * 2.0 / zoom; updateUniforms(); lastMouse.x = event.clientX; lastMouse.y = event.clientY; } } function onMouseUp(event) { isMouseDown = false; } function onWheel(event) { event.preventDefault(); const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1; zoom *= zoomFactor; updateUniforms(); } function onDoubleClick(event) { // Zoom in on clicked point const rect = renderer.domElement.getBoundingClientRect(); const x = ((event.clientX - rect.left) / rect.width) * 2 - 1; const y = -((event.clientY - rect.top) / rect.height) * 2 + 1; const aspectRatio = window.innerWidth / window.innerHeight; center.x += x * aspectRatio / zoom; center.y += y / zoom; zoom *= 2.0; updateUniforms(); } let touches = []; function onTouchStart(event) { event.preventDefault(); touches = Array.from(event.touches); } function onTouchMove(event) { event.preventDefault(); const currentTouches = Array.from(event.touches); if (touches.length === 1 && currentTouches.length === 1) { // Pan const deltaX = (currentTouches[0].clientX - touches[0].clientX) / window.innerWidth; const deltaY = (currentTouches[0].clientY - touches[0].clientY) / window.innerHeight; const aspectRatio = window.innerWidth / window.innerHeight; center.x -= deltaX * 2.0 * aspectRatio / zoom; center.y += deltaY * 2.0 / zoom; updateUniforms(); } else if (touches.length === 2 && currentTouches.length === 2) { // Pinch zoom const dist1 = Math.hypot( touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY ); const dist2 = Math.hypot( currentTouches[0].clientX - currentTouches[1].clientX, currentTouches[0].clientY - currentTouches[1].clientY ); const zoomFactor = dist2 / dist1; zoom *= zoomFactor; updateUniforms(); } touches = currentTouches; } function onTouchEnd(event) { event.preventDefault(); touches = Array.from(event.touches); } function onWindowResize() { const width = window.innerWidth; const height = window.innerHeight; renderer.setSize(width, height); material.uniforms.u_resolution.value.set(width, height); } function updateUniforms() { material.uniforms.u_center.value.set(center.x, center.y); material.uniforms.u_zoom.value = zoom; material.uniforms.u_maxIterations.value = maxIterations; } function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } // Control functions function resetView() { center.x = -0.5; center.y = 0.0; zoom = 1.0; updateUniforms(); } function increaseIterations() { maxIterations = Math.min(maxIterations + 50, 1000); document.getElementById('iterCount').textContent = maxIterations; updateUniforms(); } function decreaseIterations() { maxIterations = Math.max(maxIterations - 50, 50); document.getElementById('iterCount').textContent = maxIterations; updateUniforms(); } // Initialize the application init(); </script> </body> </html> ``` This interactive Mandelbrot explorer features: **Core Functionality:** - Real-time GPU-accelerated Mandelbrot calculation using Three.js shaders - Smooth pan and zoom controls with mouse/touch support - Deep red to yellow gradient mapping based on escape-time iterations **Interactive Controls:** - **Mouse:** Left-click and drag to pan, scroll wheel to zoom, double-click to zoom into a point - **Touch:** Single finger to pan, pinch to zoom - **Buttons:** Reset view, adjust iteration count for detail level **Visual Features:** - Smooth color gradient from deep red (low iterations) to bright yellow (high iterations) - Anti-aliased smooth iteration counting for better visual quality - Black color for points in the Mandelbrot set - Responsive design that works on desktop and mobile **Technical Implementation:** - Fragment shader performs Mandelbrot calculations on GPU for performance - Orthographic camera setup for pixel-perfect rendering - Proper aspect ratio handling for different screen sizes - Optimized rendering loop with requestAnimationFrame The explorer starts with the classic Mandelbrot view and allows you to dive deep into the fractal's infinite complexity with smooth, real-time navigation.
Here's a complete HTML implementation of an interactive Mandelbrot explorer using Three.js with the requested color gradient and controls: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mandelbrot Explorer</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } #info { position: absolute; top: 10px; left: 10px; color: white; background: rgba(0,0,0,0.5); padding: 5px; font-family: monospace; } </style> </head> <body> <div id="info">Click and drag to pan. Mouse wheel to zoom.</div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> <script> // Scene setup const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Controls const controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableZoom = true; controls.enableRotate = false; controls.enablePan = true; controls.screenSpacePanning = true; // Mandelbrot shader const mandelbrotShader = { uniforms: { center: { value: new THREE.Vector2(-0.5, 0) }, scale: { value: 2.0 }, maxIterations: { value: 100 } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 center; uniform float scale; uniform int maxIterations; varying vec2 vUv; vec3 gradient(float t) { // Deep red to yellow gradient vec3 deepRed = vec3(0.55, 0.05, 0.0); vec3 yellow = vec3(1.0, 0.95, 0.2); return mix(deepRed, yellow, t); } void main() { vec2 c = center + (vUv - 0.5) * scale; vec2 z = vec2(0.0); int i; for(i = 0; i < maxIterations; i++) { float x = (z.x * z.x - z.y * z.y) + c.x; float y = (z.y * z.x + z.x * z.y) + c.y; if((x * x + y * y) > 4.0) break; z.x = x; z.y = y; } if(i == maxIterations) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Inside color (black) } else { float hue = float(i) / float(maxIterations); vec3 color = gradient(pow(hue, 0.5)); // Apply gradient gl_FragColor = vec4(color, 1.0); } } ` }; // Create plane with Mandelbrot shader const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: mandelbrotShader.uniforms, vertexShader: mandelbrotShader.vertexShader, fragmentShader: mandelbrotShader.fragmentShader }); const plane = new THREE.Mesh(geometry, material); scene.add(plane); // Camera position camera.position.z = 1; // Mouse interaction variables let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; // Mouse event handlers renderer.domElement.addEventListener('wheel', (event) => { event.preventDefault(); const factor = event.deltaY > 0 ? 1.2 : 0.8; material.uniforms.scale.value *= factor; }); renderer.domElement.addEventListener('mousedown', (event) => { isDragging = true; previousMousePosition = { x: event.clientX, y: event.clientY }; }); renderer.domElement.addEventListener('mousemove', (event) => { if (!isDragging) return; const deltaX = event.clientX - previousMousePosition.x; const deltaY = event.clientY - previousMousePosition.y; const center = material.uniforms.center.value; const scale = material.uniforms.scale.value; center.x -= deltaX * 0.002 * scale; center.y += deltaY * 0.002 * scale; previousMousePosition = { x: event.clientX, y: event.clientY }; }); renderer.domElement.addEventListener('mouseup', () => { isDragging = false; }); renderer.domElement.addEventListener('mouseleave', () => { isDragging = false; }); // Update info display function updateInfo() { document.getElementById('info').innerHTML = ` Position: (${material.uniforms.center.value.x.toFixed(4)}, ${material.uniforms.center.value.y.toFixed(4)})<br> Zoom: ${(2.0 / material.uniforms.scale.value).toFixed(2)}x<br> Iterations: ${material.uniforms.maxIterations.value} `; } // Animation loop function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); updateInfo(); } // Handle window resize window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // Start animation animate(); </script> </body> </html> ``` This implementation includes: 1. A Three.js scene with a plane covering the viewport 2. A GLSL shader that calculates the Mandelbrot set with: - A smooth deep-red-to-yellow color gradient for escape iterations - Black coloring for points inside the Mandelbrot set 3. Interactive controls: - Click and drag to pan the view - Mouse wheel to zoom in/out 4. An info panel showing current position, zoom level, and iteration count 5. Responsive design that adjusts to window resizing The gradient is implemented using a power function (pow(hue, 0.5)) to make the color transitions smoother. You can adjust the maxIterations value in the shader uniforms to change the level of detail.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mandelbrot Explorer – Three.js</title> <style> html,body{margin:0;height:100%;overflow:hidden;background:#000;color:#fff;font-family:sans-serif} #info{ position:fixed;top:10px;left:10px;pointer-events:none; background:rgba(0,0,0,.35);padding:.4em .7em;border-radius:4px;font-size:13px; } </style> </head> <body> <div id="info"> • Mouse-wheel: zoom<br> • Drag: pan<br> • R: reset view </div> <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script> <script> (() => { /* ---------- Three.js boilerplate ---------- */ const renderer = new THREE.WebGLRenderer({antialias:true}); document.body.appendChild(renderer.domElement); const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1); // full-screen quad, no depth /* ---------- Shader material ---------- */ const MAX_ITER = 800; const uniforms = { uCenter : { value: new THREE.Vector2( -0.5, 0.0 ) }, uScale : { value: 2.5 }, // half-height of view in complex plane uAspect : { value: window.innerWidth / window.innerHeight }, uIter : { value: MAX_ITER } }; const material = new THREE.ShaderMaterial({ uniforms, fragmentShader:` uniform vec2 uCenter; uniform float uScale; uniform float uAspect; uniform int uIter; // output: linear blend deep-red -> yellow vec3 palette(float t){ vec3 deepRed = vec3(0.5,0.0,0.0); vec3 yellow = vec3(1.0,1.0,0.0); return mix(deepRed, yellow, pow(t,0.5)); // gentle gamma curve } void main(){ // map fragment to complex plane vec2 uv = gl_FragCoord.xy / vec2(textureSize(sampler2D(0),0)); // not available; we'll compute manually outside }`, // we'll replace shader below after we can compute gl_FragCoord implicitly; simpler use varying vUv }); })(); </script> </body> </html> ``` Hold on; above incomplete. We need final code. Let's continue. But we changed to compute gl_FragCoord; easier use geometry with plane and pass vUv. We'll implement. Let's craft final code again.
Here's a complete HTML file with Three.js to create an interactive Mandelbrot explorer with pan-and-zoom functionality and a deep-red-to-yellow gradient for escape-time iterations. The code uses a shader to compute the Mandelbrot set directly on the GPU for performance, and includes mouse controls for panning and zooming. Everything is contained in a single code block with necessary CDN links for Three.js. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mandelbrot Explorer</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } #info { position: absolute; top: 10px; left: 10px; color: white; font-family: Arial, sans-serif; background: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; user-select: none; } </style> </head> <body> <div id="info"> Mandelbrot Explorer<br> Click and drag to pan<br> Scroll to zoom<br> Position: <span id="pos">0, 0</span><br> Zoom: <span id="zoom">1x</span> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script type="x-shader/x-vertex" id="vertexShader"> varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } </script> <script type="x-shader/x-fragment" id="fragmentShader"> uniform vec2 uCenter; uniform float uScale; varying vec2 vUv; void main() { vec2 c = uCenter + (vUv - 0.5) * uScale * 4.0; vec2 z = vec2(0.0, 0.0); float iterations = 0.0; float maxIter = 100.0; for(float i = 0.0; i < 100.0; i++) { if(dot(z, z) > 4.0) break; float xtemp = z.x * z.x - z.y * z.y + c.x; z.y = 2.0 * z.x * z.y + c.y; z.x = xtemp; iterations = i; } // Smooth iteration count for better coloring float smoothIter = iterations + 1.0 - log2(log2(dot(z, z))); float t = smoothIter / maxIter; // Deep red to yellow gradient vec3 color; if(iterations >= maxIter - 1.0) { color = vec3(0.0, 0.0, 0.0); // Inside set: black } else { float red = 0.6 + 0.4 * t; float green = t * 0.8; float blue = t * 0.2; color = vec3(red, green, blue); } gl_FragColor = vec4(color, 1.0); } </script> <script> const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0.1, 1000); camera.position.set(0, 0, 1); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const geometry = new THREE.PlaneGeometry(4, 4); const material = new THREE.ShaderMaterial({ vertexShader: document.getElementById('vertexShader').textContent, fragmentShader: document.getElementById('fragmentShader').textContent, uniforms: { uCenter: { value: new THREE.Vector2(0.0, 0.0) }, uScale: { value: 1.0 } } }); const plane = new THREE.Mesh(geometry, material); scene.add(plane); // Interaction variables let center = new THREE.Vector2(0.0, 0.0); let scale = 1.0; let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; // Mouse and scroll event handlers renderer.domElement.addEventListener('mousedown', (e) => { isDragging = true; previousMousePosition = { x: (e.clientX / window.innerWidth) * 2 - 1, y: -(e.clientY / window.innerHeight) * 2 + 1 }; }); renderer.domElement.addEventListener('mousemove', (e) => { if (isDragging) { const mouseX = (e.clientX / window.innerWidth) * 2 - 1; const mouseY = -(e.clientY / window.innerHeight) * 2 + 1; const deltaX = (mouseX - previousMousePosition.x) * scale * 2; const deltaY = (mouseY - previousMousePosition.y) * scale * 2; center.x -= deltaX; center.y += deltaY; previousMousePosition = { x: mouseX, y: mouseY }; updateUniforms(); } }); renderer.domElement.addEventListener('mouseup', () => { isDragging = false; }); renderer.domElement.addEventListener('mouseleave', () => { isDragging = false; }); renderer.domElement.addEventListener('wheel', (e) => { e.preventDefault(); const zoomSpeed = 0.1; const zoomFactor = e.deltaY > 0 ? 1.0 + zoomSpeed : 1.0 - zoomSpeed; scale *= zoomFactor; scale = Math.max(0.0000001, Math.min(10, scale)); // Prevent extreme zooming updateUniforms(); }, { passive: false }); function updateUniforms() { material.uniforms.uCenter.value = center; material.uniforms.uScale.value = scale; // Update info display document.getElementById('pos').textContent = `${center.x.toFixed(4)}, ${center.y.toFixed(4)}`; document.getElementById('zoom').textContent = `${(1/scale).toFixed(2)}x`; } window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); camera.left = -2 * window.innerWidth / window.innerHeight; camera.right = 2 * window.innerWidth / window.innerHeight; camera.top = 2; camera.bottom = -2; camera.updateProjectionMatrix(); }); function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } updateUniforms(); animate(); </script> </body> </html> ``` This code creates a fully interactive Mandelbrot explorer using Three.js via a CDN. The Mandelbrot set is rendered with a shader for efficiency, featuring a smooth deep-red-to-yellow gradient based on escape-time iterations. You can pan by clicking and dragging with the mouse, and zoom using the mouse wheel. A small info panel in the top-left corner displays the current center coordinates and zoom level. The implementation handles window resizing and provides a smooth user experience.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Mandelbrot Explorer (Three.js + Shader)</title> <style> html, body { margin: 0; padding: 0; width: 100vw; height: 100vh; overflow: hidden; background: #000; } #canvas { display: block; width: 100vw; height: 100vh; } #instructions { color: #fff; position: absolute; top: 10px; left: 10px; z-index: 2; background: rgba(0,0,0,0.5); padding: 8px 14px; border-radius: 10px; font: 16px sans-serif; pointer-events: none; user-select: none; } </style> </head> <body> <div id="instructions"> <b>Mandelbrot Explorer</b><br> Mouse: Drag = Pan<br> Wheel / Pinch = Zoom<br> Double-click: Reset View </div> <script src="https://cdn.jsdelivr.net/npm/three@0.154.0/build/three.min.js"></script> <script> let renderer, scene, camera, material, mesh; const MAX_ITER = 512; let center = { x: -0.5, y: 0 }; // Center of the view (mandelbrot) let scale = 2.5; // Width of view in complex plane (zoom) const canvas = document.createElement('canvas'); canvas.id = 'canvas'; document.body.appendChild(canvas); // Set up renderer renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); // Set up ortho camera function getAspect() { return window.innerWidth / window.innerHeight; } camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // Full-screen quad geometry const geometry = new THREE.PlaneGeometry(2, 2); // Mandelbrot fragment shader const fragmentShader = ` precision highp float; uniform vec2 u_center; uniform float u_scale; uniform float u_aspect; uniform int u_maxIter; // Smooth coloring function using normalized continuous iteration count float mandelbrot(vec2 c) { vec2 z = vec2(0.0, 0.0); float iter = 0.0; float nu = 0.0; for (int i=0; i<1024; ++i) { if (i >= u_maxIter) break; float x = (z.x * z.x - z.y * z.y) + c.x; float y = (2.0 * z.x * z.y) + c.y; if ((x*x + y*y) > 256.0) { nu = float(i) - log2(log(length(vec2(x, y)))) + 4.0; iter = nu; break; } z.x = x; z.y = y; } return iter; } // Red-yellow gradient (deep red to yellow) vec3 colorMap(float t) { // t = 0 : deep red, t = 1 : yellow (RGB: 1,1,0) // We'll use a sqrt mapping for brightness t = sqrt(clamp(t, 0.0, 1.0)); // Deep red: (0.45, 0.02, 0.02), Yellow: (1.0, 1.0, 0.0) vec3 deepRed = vec3(0.45, 0.02, 0.02); vec3 yellow = vec3(1.0, 1.0, 0.0); return mix(deepRed, yellow, t); } void main() { // Map from [-1, 1] screen to complex plane vec2 uv = gl_FragCoord.xy / vec2(textureSize(gl_FragCoord, 0)); float x = (gl_FragCoord.x / float(${window.innerWidth}) - 0.5) * u_aspect * u_scale + u_center.x; float y = (gl_FragCoord.y / float(${window.innerHeight}) - 0.5) * u_scale + u_center.y; vec2 c = vec2(x, y); float iter = mandelbrot(c); float t = iter / float(u_maxIter); // Mandelbrot set interior: black if (iter < 1.0) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { gl_FragColor = vec4(colorMap(t), 1.0); } } `; // Vertex shader (simple passthrough) const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); } `; // Set up shader material let uniforms = { u_center: { value: new THREE.Vector2(center.x, center.y) }, u_scale: { value: scale }, u_aspect: { value: getAspect() }, u_maxIter: { value: MAX_ITER } }; material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, }); // Plane mesh mesh = new THREE.Mesh(geometry, material); scene = new THREE.Scene(); scene.add(mesh); // Resize handler function onResize() { renderer.setSize(window.innerWidth, window.innerHeight, false); uniforms.u_aspect.value = getAspect(); } window.addEventListener('resize', onResize, false); onResize(); // --- PAN & ZOOM INTERACTIONS --- let isPanning = false; let lastMouse = { x: 0, y: 0 }; let lastCenter = { x: center.x, y: center.y }; let pinchStartDist = null, pinchStartScale = null; function screenToWorld(x, y) { // screen (0..w, 0..h) to complex plane const aspect = getAspect(); let wx = (x / window.innerWidth - 0.5) * aspect * scale + center.x; let wy = (y / window.innerHeight - 0.5) * scale + center.y; return { x: wx, y: wy }; } canvas.addEventListener('mousedown', (e) => { isPanning = true; lastMouse.x = e.clientX; lastMouse.y = e.clientY; lastCenter.x = center.x; lastCenter.y = center.y; }); window.addEventListener('mousemove', (e) => { if (!isPanning) return; let dx = e.clientX - lastMouse.x; let dy = e.clientY - lastMouse.y; // Translate pixels to complex plane units center.x = lastCenter.x - dx * scale * getAspect() / window.innerWidth; center.y = lastCenter.y - dy * scale / window.innerHeight; uniforms.u_center.value.set(center.x, center.y); requestRenderIfNotRequested(); }); window.addEventListener('mouseup', (e) => { isPanning = false; }); // Mouse wheel zoom canvas.addEventListener('wheel', (e) => { e.preventDefault(); // Zoom on mouse position let zoomFactor = Math.exp(-e.deltaY * 0.001); let mx = e.offsetX, my = e.offsetY; let before = screenToWorld(mx, my); scale *= 1 / zoomFactor; uniforms.u_scale.value = scale; let after = screenToWorld(mx, my); center.x += before.x - after.x; center.y += before.y - after.y; uniforms.u_center.value.set(center.x, center.y); requestRenderIfNotRequested(); }, { passive: false }); // Touch events for mobile: pan and pinch zoom canvas.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; pinchStartDist = Math.sqrt(dx * dx + dy * dy); pinchStartScale = scale; } else if (e.touches.length === 1) { isPanning = true; lastMouse.x = e.touches[0].clientX; lastMouse.y = e.touches[0].clientY; lastCenter.x = center.x; lastCenter.y = center.y; } }, { passive: false }); canvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (e.touches.length === 2 && pinchStartDist !== null) { // Pinch zoom const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const dist = Math.sqrt(dx * dx + dy * dy); let zoomFactor = pinchStartDist / dist; scale = pinchStartScale * zoomFactor; uniforms.u_scale.value = scale; requestRenderIfNotRequested(); } else if (e.touches.length === 1 && isPanning) { // Pan let dx = e.touches[0].clientX - lastMouse.x; let dy = e.touches[0].clientY - lastMouse.y; center.x = lastCenter.x - dx * scale * getAspect() / window.innerWidth; center.y = lastCenter.y - dy * scale / window.innerHeight; uniforms.u_center.value.set(center.x, center.y); requestRenderIfNotRequested(); } }, { passive: false }); window.addEventListener('touchend', (e) => { isPanning = false; pinchStartDist = null; pinchStartScale = null; }); // Double-click to reset canvas.addEventListener('dblclick', (e) => { center = { x: -0.5, y: 0 }; scale = 2.5; uniforms.u_center.value.set(center.x, center.y); uniforms.u_scale.value = scale; requestRenderIfNotRequested(); }); // --- RENDER LOOP --- let renderRequested = false; function render() { renderer.render(scene, camera); renderRequested = false; } function requestRenderIfNotRequested() { if (!renderRequested) { renderRequested = true; requestAnimationFrame(render); } } // Initial render render(); // Hardware acceleration could enable animation, but Mandelbrot is static unless you move! // User interaction will re-render. </script> </body> </html> ``` **Instructions**: Scroll/pinch to zoom, drag to pan, double-click to reset. The gradient maps deep red for near the set, yellow for fast escapes. Everything runs on the GPU for super-fast exploration!
Below is a complete HTML file that creates an interactive Mandelbrot explorer using Three.js. It includes pan-and-zoom controls and a smooth deep-red-to-yellow gradient mapped to escape-time iterations. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mandelbrot Explorer with Three.js</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Scene setup const scene = new THREE.Scene(); const camera = new THREE.Orth
### Interactive Mandelbrot Explorer #### Overview The following code creates an interactive Mandelbrot explorer using Three.js. The explorer features pan-and-zoom controls and a smooth color gradient mapped to escape-time iterations. #### Code ```html <!DOCTYPE html> <html> <head> <title>Mandelbrot Explorer</title> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> let camera, scene, renderer, plane; let xmin = -2.5, xmax = 1.5, ymin = -1.5, ymax = 1.5; let width = window.innerWidth, height = window.innerHeight; init(); animate(); function init() { camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); scene = new THREE.Scene(); const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: { xmin: { value: xmin }, xmax: { value: xmax }, ymin: { value: ymin }, ymax: { value: ymax }, maxIterations: { value: 256 } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` varying vec2 vUv; uniform float xmin, xmax, ymin, ymax; uniform int maxIterations; vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } void main() { float x0 = xmin + (xmax - xmin) * vUv.x; float y0 = ymin + (ymax - ymin) * vUv.y; float x = 0.0, y = 0.0; int iteration = 0; while (x * x + y * y < 4.0 && iteration < maxIterations) { float xtemp = x * x - y * y + x0; y = 2.0 * x * y + y0; x = xtemp; iteration++; } if (iteration == maxIterations) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { float hue = float(iteration) / float(maxIterations); vec3 rgb = hsv2rgb(vec3(hue * 0.15 + 0.0, 1.0, 1.0)); // deep-red to yellow gradient gl_FragColor = vec4(rgb, 1.0); } } ` }); plane = new THREE.Mesh(geometry, material); scene.add(plane); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); document.body.appendChild(renderer.domElement); window.addEventListener('resize', onWindowResize); window.addEventListener('wheel', onWheel); window.addEventListener('mousedown', onMouseDown); window.addEventListener('mousemove', onMouseMove); } function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } function onWindowResize() { width = window.innerWidth; height = window.innerHeight; renderer.setSize(width, height); } let mouseX, mouseY, isDragging = false; function onMouseDown(event) { isDragging = true; mouseX = event.clientX; mouseY = event.clientY; } function onMouseMove(event) { if (isDragging) { let dx = (event.clientX - mouseX) / width * (xmax - xmin); let dy = (event.clientY - mouseY) / height * (ymax - ymin); xmin -= dx; xmax -= dx; ymin -= dy; ymax -= dy; mouseX = event.clientX; mouseY = event.clientY; plane.material.uniforms.xmin.value = xmin; plane.material.uniforms.xmax.value = xmax; plane.material.uniforms.ymin.value = ymin; plane.material.uniforms.ymax.value = ymax; } } function onWheel(event) { let scale = 1.0 - event.deltaY * 0.001; let x = (event.clientX / width) * (xmax - xmin) + xmin; let y = (event.clientY / height) * (ymax - ymin) + ymin; let newWidth = (xmax - xmin) * scale; let newHeight = (ymax - ymin) * scale; xmin = x - (x - xmin) * scale; xmax = x + (xmax - x) * scale; ymin = y - (y - ymin) * scale; ymax = y + (ymax - y) * scale; plane.material.uniforms.xmin.value = xmin; plane.material.uniforms.xmax.value = xmax; plane.material.uniforms.ymin.value = ymin; plane.material.uniforms.ymax.value = ymax; } document.addEventListener('mouseup', () => isDragging = false); </script> </body> </html> ``` #### Explanation The provided code creates a full-screen Mandelbrot explorer using Three.js. The `init` function initializes the scene, camera, and renderer. It also defines the plane geometry and the shader material used to render the Mandelbrot set. The fragment shader is responsible for calculating the color of each pixel based on the number of iterations it takes for the corresponding complex number to escape the Mandelbrot set. The color is chosen from a smooth deep-red-to-yellow gradient. The `onWheel` function handles mouse wheel events to zoom in and out of the Mandelbrot set, while the `onMouseDown` and `onMouseMove` functions handle mouse events to pan the view. The `animate` function continuously renders the scene. You can run this code in a web browser to interact with the Mandelbrot explorer. Use the mouse wheel to zoom and drag the mouse to pan.
# Interactive Mandelbrot Explorer Here's an interactive Mandelbrot explorer with pan, zoom, and smooth gradient coloring using Three.js: ```html <!DOCTYPE html> <html> <head> <title>Interactive Mandelbrot Explorer</title> <style> body { margin: 0; overflow: hidden; background: #000; font-family: Arial, sans-serif; } #info { position: absolute; top: 10px; left: 10px; color: white; font-size: 14px; background: rgba(0,0,0,0.7); padding: 8px; border-radius: 4px; } canvas { display: block; } </style> </head> <body> <div id="info"> <div>Controls: Drag to pan | Scroll to zoom | Double-click to reset</div> <div>Scale: <span id="scaleVal">4</span></div> </div> <script src="https://cdn.jsdelivr.net/npm/three@0.154.0/build/three.min.js"></script> <script> // Main Javascript code window.addEventListener('load', () => { // Set up Three.js components const scene = new THREE.Scene(); const container = document.body; // Initialize renderer const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x000000); container.appendChild(renderer.domElement); // Orthographic camera setup const camera = new THREE.OrthographicCamera( -1, 1, 1, -1, -10, 10 ); camera.position.z = 1; // Uniforms for shader parameters const uniforms = { u_center: { value: new THREE.Vector2(-0.5, 0.0) }, u_scale: { value: 4.0 }, u_aspect: { value: window.innerWidth / window.innerHeight }, u_max_iter: { value: 100 }, u_color_range: { value: new THREE.Vector4(0.5, 0.0, 0.0, 1.0) }, u_color_factor: { value: new THREE.Vector2(0.52, 1.0) }, u_time: { value: 0 } }; // Update scale display document.getElementById('scaleVal').textContent = uniforms.u_scale.value.toFixed(6); // Create plane geometry const geometry = new THREE.PlaneGeometry(2, 2); // Mandelbrot shader material const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 u_center; uniform float u_scale; uniform float u_aspect; uniform float u_max_iter; uniform vec4 u_color_range; uniform vec2 u_color_factor; uniform float u_time; void main() { // Adjust coordinates based on scale and aspect ratio vec2 c = vec2( (gl_FragCoord.x / u_aspect - 0.5 * u_aspect) * u_scale / u_aspect, (gl_FragCoord.y / u_aspect - 0.5) * u_scale ) + u_center; vec2 z = vec2(0.0); float iter = 0.0; // Mandelbrot iteration for (float i = 0.0; i < u_max_iter; i++) { if (dot(z, z) > 4.0) break; // z_{n+1} = z_n^2 + c z = vec2( z.x * z.x - z.y * z.y + c.x, 2.0 * z.x * z.y + c.y ); iter++; } // Compute color based on iterations if (iter >= u_max_iter) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Inside: black } else { // Normalized iteration depth with smoothing float t = (iter + 1.0 - log(log(length(z))) / log(2.0)) / u_max_iter; // Smooth gradient from deep red to yellow vec3 deepRed = vec3(u_color_range.x, u_color_range.y, u_color_range.z); vec3 yellow = vec3(1.0, 1.0, 0.0); vec3 color = mix(deepRed, yellow, pow(t * u_color_factor.x, u_color_factor.y)); // Subtle motion effect based on time color += sin(t * 10.0 + u_time) * 0.05; gl_FragColor = vec4(color, 1.0); } } ` }); // Create fractal plane const plane = new THREE.Mesh(geometry, material); scene.add(plane); // Interaction variables let isDragging = false; let lastMousePosition = { x: 0, y: 0 }; // Mouse event handlers const onMouseDown = (e) => { isDragging = true; lastMousePosition = { x: e.clientX, y: e.clientY }; // Create elastic effect on click animateClickEffect(); }; const animateClickEffect = () => { let progress = 0; const duration = 500; const initialScale = uniforms.u_scale.value; const startTime = Date.now(); const animate = () => { if (progress < 1) { requestAnimationFrame(animate); } const elapsed = Date.now() - startTime; progress = Math.min(elapsed / duration, 1); const scaleFactor = 1 - Math.sin(progress * Math.PI) * 0.15; uniforms.u_scale.value = initialScale * scaleFactor; uniforms.u_time.value = progress * 10; updateScaleDisplay(); renderer.render(scene, camera); }; animate(); }; const onMouseMove = (e) => { if (!isDragging) return; if (!e.buttons) { isDragging = false; return; } const dx = -(e.clientX - lastMousePosition.x) / window.innerWidth; const dy = (e.clientY - lastMousePosition.y) / window.innerHeight; // Apply scaling factor for natural-feeling pan const panSpeed = uniforms.u_scale.value * 1.5; uniforms.u_center.value.x += dx * panSpeed; uniforms.u_center.value.y += dy * panSpeed; lastMousePosition = { x: e.clientX, y: e.clientY }; renderer.render(scene, camera); }; const onMouseUp = () => { isDragging = false; }; const onMouseWheel = (e) => { const zoomIntensity = 0.1; const delta = Math.sign(e.deltaY); // Calculate zoom factor based on mouse position const mouseX = e.clientX / window.innerWidth; const mouseY = e.clientY / window.innerHeight; // Weight factor for natural zoom towards cursor const weight = 0.02 * uniforms.u_scale.value; const zoomFactor = 1.0 + (delta > 0 ? zoomIntensity : -zoomIntensity/3); const scaleBefore = uniforms.u_scale.value; uniforms.u_scale.value *= zoomFactor; if (delta < 0) { uniforms.u_center.value.x += (mouseX - 0.5) * weight * scaleBefore; uniforms.u_center.value.y += (0.5 - mouseY) * weight * scaleBefore; } uniforms.u_time.value += 0.2; updateScaleDisplay(); renderer.render(scene, camera); }; const onDoubleClick = () => { // Reset view to default position uniforms.u_center.value.set(-0.5, 0.0); uniforms.u_scale.value = 4.0; updateScaleDisplay(); renderer.render(scene, camera); }; const updateScaleDisplay = () => { document.getElementById('scaleVal').textContent = uniforms.u_scale.value.toFixed(6); }; // Handle window resize const onWindowResize = () => { uniforms.u_aspect.value = window.innerWidth / window.innerHeight; camera.aspect = uniforms.u_aspect.value; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.render(scene, camera); }; // Animation loop const animate = () => { requestAnimationFrame(animate); renderer.render(scene, camera); }; // Add event listeners renderer.domElement.addEventListener('mousedown', onMouseDown); renderer.domElement.addEventListener('mousemove', onMouseMove); renderer.domElement.addEventListener('mouseup', onMouseUp); renderer.domElement.addEventListener('wheel', onMouseWheel); renderer.domElement.addEventListener('dblclick', onDoubleClick); window.addEventListener('resize', onWindowResize); // Initial rendering renderer.render(scene, camera); animate(); }); </script> </body> </html> ``` ## Features 1. **Mandelbrot Visualization**: - Classic Mandelbrot fractal rendering - Deep red to yellow gradient based on escape time iterations - Smooth coloring with logarithmic smoothing - Subtle animation effects 2. **Interactive Controls**: - Pan: Click and drag anywhere to move around the fractal - Zoom: Scroll up/down with mouse wheel to zoom in/out - Reset: Double-click to return to the default view 3. **Visual Design**: - Full-screen canvas rendering - Custom gradient coloring from deep red to yellow - Real-time visual feedback during interactions - Responsive design for all screen sizes The implementation uses an orthographic camera and leverages Three.js's shader capabilities to efficiently calculate the Mandelbrot set directly on the GPU. The fractal updates smoothly as you explore different regions with pan and zoom controls.