This benchmark tests an LLM's ability to handle a multi-language, algorithmically complex task. It requires generating a single HTML file with JavaScript (using Three.js) to manage the scene, and GLSL shader code to render a dynamic, interactive fractal. This evaluates advanced knowledge of mathematics, GPU programming, and system integration.
Prompt
You are a creative coder and an expert in computer graphics, specializing in Three.js and GLSL shaders. Mission: Create a visually stunning, interactive, and animated fractal art piece. The output must be a single, self-contained HTML file. Mandatory Technical & Creative Specifications: Technology: Use the Three.js library, loaded from a CDN. The core fractal rendering logic must be implemented in a GLSL fragment shader, not in JavaScript. The Fractal: Render a Julia Set fractal. The complex constant c that defines the Julia Set's shape must animate smoothly over time, causing the fractal to continuously morph and evolve. The animation should be a slow, circular path in the complex plane. Visuals & Colors: The coloring of the fractal must be based on the number of iterations before a point escapes. The color palette must be a smooth gradient transitioning through "deep indigo -> magenta -> fiery orange -> golden yellow". The colors should pulse and shift slowly with time. Interactivity: Zoom: The user must be able to zoom in and out using the mouse wheel. The zoom should be centered on the cursor's position. Pan: The user must be able to pan the view by clicking and dragging the mouse. Morphing Control: The animation of the Julia Set constant c should be influenced by the mouse's X position. Moving the mouse left and right should alter the path of the animation, changing the shapes being generated. Pause: Pressing the space bar must toggle play/pause for all animations (both the morphing and the color shifting). Action Plan: Before writing any code, describe your plan, including the structure of the HTML, the JavaScript setup with Three.js, and the logic for the GLSL fragment shader (uniform variables, main rendering loop).
A system prompt was added to support web rendering
Answer guidance
A successful response is a single HTML file that runs without errors and perfectly implements all features. Checklist for a perfect score: Does it render a full-screen, animating Julia set? Is the core logic in a GLSL fragment shader? Is the color palette correct and does it shift over time? Do all interactive controls (zoom, pan, mouse-based morphing, space bar pause) work smoothly and correctly? A failure is any response that produces a static image, WebGL errors, has non-functional controls, or uses JavaScript for the main fractal calculation.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Animated Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; background: #000; } canvas { display: block; } #info { position: absolute; top: 10px; width: 100%; text-align: center; color: white; font-family: Arial, sans-serif; font-size: 14px; text-shadow: 0 0 5px rgba(0,0,0,0.5); pointer-events: none; } </style> </head> <body> <div id="info">Julia Set Fractal | Mouse: Drag to Pan, Scroll to Zoom | Space: Pause/Play</div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Main variables let scene, camera, renderer, material; let isPlaying = true; let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; // Fractal parameters const fractalParams = { zoom: 1.0, offsetX: 0.0, offsetY: 0.0, cX: -0.7, cY: 0.27015, time: 0.0, colorShift: 0.0 }; // Initialize Three.js function init() { // Create scene scene = new THREE.Scene(); // Create camera camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1; // Create renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Create shader material material = new THREE.ShaderMaterial({ uniforms: { u_zoom: { value: fractalParams.zoom }, u_offset: { value: new THREE.Vector2(fractalParams.offsetX, fractalParams.offsetY) }, u_c: { value: new THREE.Vector2(fractalParams.cX, fractalParams.cY) }, u_time: { value: fractalParams.time }, u_colorShift: { value: fractalParams.colorShift }, u_aspectRatio: { value: window.innerWidth / window.innerHeight } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float u_zoom; uniform vec2 u_offset; uniform vec2 u_c; uniform float u_time; uniform float u_colorShift; uniform float u_aspectRatio; varying vec2 vUv; // Convert HSV to RGB 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); } // Color palette function vec3 getColor(float t) { // Deep indigo -> magenta -> fiery orange -> golden yellow if (t < 0.25) { // Deep indigo to magenta return mix(vec3(0.1, 0.0, 0.3), vec3(0.8, 0.0, 0.8), t * 4.0); } else if (t < 0.5) { // Magenta to fiery orange return mix(vec3(0.8, 0.0, 0.8), vec3(1.0, 0.4, 0.0), (t - 0.25) * 4.0); } else if (t < 0.75) { // Fiery orange to golden yellow return mix(vec3(1.0, 0.4, 0.0), vec3(1.0, 0.9, 0.2), (t - 0.5) * 4.0); } else { // Cycle back to indigo with some blending return mix(vec3(1.0, 0.9, 0.2), vec3(0.1, 0.0, 0.3), (t - 0.75) * 4.0); } } void main() { // Normalize coordinates to complex plane (-2 to 2) vec2 z; z.x = (vUv.x - 0.5) * 4.0 * u_aspectRatio / u_zoom + u_offset.x; z.y = (vUv.y - 0.5) * 4.0 / u_zoom + u_offset.y; // Julia set iteration vec2 c = u_c; int iterations = 0; const int max_iterations = 256; for(int i = 0; i < max_iterations; i++) { // Complex number multiplication: (a + bi)(c + di) = (ac - bd) + (ad + bc)i float x = (z.x * z.x - z.y * z.y) + c.x; float y = (z.x * z.y + z.x * z.y) + c.y; // Check for escape condition if(x*x + y*y > 4.0) { iterations = i; break; } z.x = x; z.y = y; } // Color based on iterations if(iterations == 0) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { // Smooth iteration count for better coloring float iter = float(iterations) + 1.0 - log(log(length(z)))/log(2.0); float t = mod(iter * 0.02 + u_colorShift, 1.0); vec3 color = getColor(t); gl_FragColor = vec4(color, 1.0); } } ` }); // Create a full-screen quad const geometry = new THREE.PlaneGeometry(2, 2); const quad = new THREE.Mesh(geometry, material); scene.add(quad); // Event listeners setupEventListeners(); // Handle window resize window.addEventListener('resize', onWindowResize); // Start animation loop animate(); } function setupEventListeners() { // Mouse wheel for zoom window.addEventListener('wheel', (event) => { const zoomIntensity = 0.1; const mouseX = event.clientX / window.innerWidth; const mouseY = 1.0 - event.clientY / window.innerHeight; // Convert mouse position to complex plane coordinates const worldX = (mouseX - 0.5) * 4.0 * (window.innerWidth / window.innerHeight) / fractalParams.zoom + fractalParams.offsetX; const worldY = (mouseY - 0.5) * 4.0 / fractalParams.zoom + fractalParams.offsetY; // Apply zoom if (event.deltaY < 0) { fractalParams.zoom *= (1 + zoomIntensity); } else { fractalParams.zoom /= (1 + zoomIntensity); } // Adjust offset to zoom towards mouse position fractalParams.offsetX = worldX - (mouseX - 0.5) * 4.0 * (window.innerWidth / window.innerHeight) / fractalParams.zoom; fractalParams.offsetY = worldY - (mouseY - 0.5) * 4.0 / fractalParams.zoom; }); // Mouse drag for panning window.addEventListener('mousedown', (event) => { isDragging = true; previousMousePosition = { x: event.clientX, y: event.clientY }; }); window.addEventListener('mousemove', (event) => { if (isDragging) { const deltaX = event.clientX - previousMousePosition.x; const deltaY = event.clientY - previousMousePosition.y; // Adjust offsets based on drag direction fractalParams.offsetX -= deltaX * 4.0 / (window.innerWidth * fractalParams.zoom); fractalParams.offsetY += deltaY * 4.0 / (window.innerHeight * fractalParams.zoom); previousMousePosition = { x: event.clientX, y: event.clientY }; } else { // Influence morphing path based on mouse X position const mouseXNormalized = event.clientX / window.innerWidth; fractalParams.morphInfluence = mouseXNormalized; } }); window.addEventListener('mouseup', () => { isDragging = false; }); // Space bar to pause/play window.addEventListener('keydown', (event) => { if (event.code === 'Space') { isPlaying = !isPlaying; event.preventDefault(); } }); } function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); material.uniforms.u_aspectRatio.value = window.innerWidth / window.innerHeight; } // Animation loop function animate() { requestAnimationFrame(animate); if (isPlaying) { fractalParams.time += 0.01; fractalParams.colorShift += 0.002; // Animate Julia constant along a morphing path influenced by mouse const influence = fractalParams.morphInfluence || 0.5; const angle = fractalParams.time * (0.5 + influence * 0.5); const radius = 0.8 + Math.sin(fractalParams.time * 0.3) * 0.2; fractalParams.cX = Math.cos(angle) * radius; fractalParams.cY = Math.sin(angle * 1.2) * radius * 0.8; } // Update uniforms material.uniforms.u_zoom.value = fractalParams.zoom; material.uniforms.u_offset.value.set(fractalParams.offsetX, fractalParams.offsetY); material.uniforms.u_c.value.set(fractalParams.cX, fractalParams.cY); material.uniforms.u_time.value = fractalParams.time; material.uniforms.u_colorShift.value = fractalParams.colorShift; renderer.render(scene, camera); } // Initialize the application init(); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Interactive Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; background-color: black; } canvas { display: block; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> <script> // Initialize Three.js 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({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Create a plane geometry for the full-screen quad const geometry = new THREE.PlaneGeometry(2, 2); // Define the shaders const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragmentShader = ` uniform vec2 u_resolution; uniform float u_time; uniform vec2 u_mouse; uniform float u_zoom; uniform vec2 u_pan; uniform bool u_paused; uniform vec2 u_c; varying vec2 vUv; vec3 getColor(float iterations, float maxIterations) { float t = iterations / maxIterations; t = mod(t + u_time * 0.1, 1.0); vec3 indigo = vec3(0.0, 0.0, 0.3); vec3 magenta = vec3(1.0, 0.0, 1.0); vec3 orange = vec3(1.0, 0.5, 0.0); vec3 yellow = vec3(1.0, 1.0, 0.0); if (t < 0.33) { return mix(indigo, magenta, t * 3.0); } else if (t < 0.66) { return mix(magenta, orange, (t - 0.33) * 3.0); } else { return mix(orange, yellow, (t - 0.66) * 3.0); } } void main() { vec2 uv = vUv * 2.0 - 1.0; uv.x *= u_resolution.x / u_resolution.y; vec2 z = uv / u_zoom + u_pan; vec2 c = u_c; float maxIterations = 100.0; float iterations = 0.0; for (float i = 0.0; i < maxIterations; i++) { float x = z.x * z.x - z.y * z.y + c.x; float y = 2.0 * z.x * z.y + c.y; z = vec2(x, y); if (dot(z, z) > 4.0) { iterations = i; break; } } if (iterations < maxIterations) { vec3 color = getColor(iterations, maxIterations); gl_FragColor = vec4(color, 1.0); } else { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } } `; // Create shader material const uniforms = { u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_time: { value: 0.0 }, u_mouse: { value: new THREE.Vector2(0.5, 0.5) }, u_zoom: { value: 1.0 }, u_pan: { value: new THREE.Vector2(0.0, 0.0) }, u_paused: { value: false }, u_c: { value: new THREE.Vector2(-0.7, 0.27) } }; const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Handle window resize window.addEventListener('resize', () => { const width = window.innerWidth; const height = window.innerHeight; renderer.setSize(width, height); uniforms.u_resolution.value.set(width, height); }); // Mouse wheel for zoom (centered on cursor) window.addEventListener('wheel', (event) => { event.preventDefault(); const zoomSpeed = 0.1; const delta = -Math.sign(event.deltaY) * zoomSpeed; const newZoom = uniforms.u_zoom.value * (1 + delta); const mouseX = (event.clientX / window.innerWidth) * 2 - 1; const mouseY = -((event.clientY / window.innerHeight) * 2 - 1); const aspect = window.innerWidth / window.innerHeight; const uvMouse = new THREE.Vector2(mouseX * aspect, mouseY); const currentZoom = uniforms.u_zoom.value; const currentPan = uniforms.u_pan.value; const zMouse = new THREE.Vector2( uvMouse.x / currentZoom + currentPan.x, uvMouse.y / currentZoom + currentPan.y ); const newPan = new THREE.Vector2( zMouse.x - uvMouse.x / newZoom, zMouse.y - uvMouse.y / newZoom ); uniforms.u_zoom.value = newZoom; uniforms.u_pan.value.copy(newPan); }); // Mouse drag for pan let isDragging = false; let lastMousePosition = new THREE.Vector2(); window.addEventListener('mousedown', (event) => { isDragging = true; lastMousePosition.set(event.clientX, event.clientY); }); window.addEventListener('mousemove', (event) => { uniforms.u_mouse.value.set(event.clientX / window.innerWidth, 1.0 - event.clientY / window.innerHeight); if (isDragging) { const deltaX = (event.clientX - lastMousePosition.x) / window.innerWidth; const deltaY = (event.clientY - lastMousePosition.y) / window.innerHeight; const aspect = window.innerWidth / window.innerHeight; uniforms.u_pan.value.x -= deltaX * 2 * aspect / uniforms.u_zoom.value; uniforms.u_pan.value.y += deltaY * 2 / uniforms.u_zoom.value; lastMousePosition.set(event.clientX, event.clientY); } }); window.addEventListener('mouseup', () => { isDragging = false; }); // Space bar to toggle pause window.addEventListener('keydown', (event) => { if (event.code === 'Space') { uniforms.u_paused.value = !uniforms.u_paused.value; } }); // Animation loop let time = 0; function animate() { requestAnimationFrame(animate); if (!uniforms.u_paused.value) { time += 0.01; uniforms.u_time.value = time; const speed = 0.5; const mouseX = uniforms.u_mouse.value.x; const centerX = -0.7 + (mouseX - 0.5) * 0.4; const angle = time * speed; const radius = 0.1; const cReal = centerX + radius * Math.cos(angle); const cImag = 0.27 + radius * Math.sin(angle); uniforms.u_c.value.set(cReal, cImag); } renderer.render(scene, camera); } animate(); </script> </body> </html> ```
Plan ------ 1. HTML skeleton - A minimal page with a full-screen canvas. - Three.js loaded from a CDN. - A tiny bit of CSS to remove margins. 2. JavaScript (Three.js) - Create a `WebGLRenderer`, `Scene`, and an `OrthographicCamera` filling the viewport. - A single `PlaneGeometry` (two triangles) fills the screen; its material uses a custom `ShaderMaterial`. - Uniforms: – `u_resolution` (vec2) – viewport size. – `u_center` (vec2) – complex-plane center under pan/zoom. – `u_scale` (float) – zoom level. – `u_c` (vec2) – current Julia constant. – `u_time` (float) – global time for color cycling and morphing. – `u_paused` (float) – 0/1 flag toggled by space bar. - Event listeners: – `wheel` → zoom around cursor. – `pointerdown / pointermove / pointerup` → pan. – `keydown` (Space) → toggle `u_paused`. 3. GLSL fragment shader - `main()` maps pixel → complex plane using `u_center`, `u_scale`, `u_resolution`. - Julia iteration: `z = z² + c` until `|z| > 2` or `max_iter` reached. - `c` is animated: base = slow circle `0.7885 * (cos(t), sin(t))` mouse influence = `0.1 * (mouseX - 0.5)` added to the circle’s radius. - Color: smooth iteration count → 0…1 → 4-stop gradient (indigo → magenta → orange → yellow). - Palette shifts over time: `hue_shift = sin(u_time * 0.3) * 0.1`. - If `u_paused > 0.5`, freeze `u_time` and `u_c`. 4. Self-contained file - All code (HTML, JS, GLSL) in one `<script type="x-shader/x-fragment">` block and inline JS. Code ------ ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>Animated Julia Set</title> <style>html,body{margin:0;height:100%;overflow:hidden;background:#000}</style> </head> <body> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script> <script type="module"> import * as THREE from 'three'; const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); } `; const fragmentShader = ` precision highp float; uniform vec2 u_resolution; uniform vec2 u_center; uniform float u_scale; uniform vec2 u_c; uniform float u_time; uniform float u_paused; vec3 palette(float t) { vec3 a = vec3(0.15, 0.05, 0.35); // deep indigo vec3 b = vec3(0.85, 0.20, 0.60); // magenta vec3 c = vec3(1.00, 0.45, 0.15); // fiery orange vec3 d = vec3(1.00, 0.85, 0.20); // golden yellow t = mod(t + sin(u_time * 0.3) * 0.1, 1.0); t *= 3.0; vec3 col; if (t < 1.0) { col = mix(a, b, t); } else if (t < 2.0) { col = mix(b, c, t - 1.0); } else { col = mix(c, d, t - 2.0); } return col; } void main() { vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); vec2 z = u_center + uv * u_scale; float t = u_paused > 0.5 ? 0.0 : u_time * 0.1; vec2 c = u_c + vec2(cos(t), sin(t)) * 0.7885; float iter = 0.0; const float max_iter = 200.0; vec2 z0 = z; for (float i = 0.0; i < max_iter; i++) { if (dot(z, z) > 4.0) break; z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; iter += 1.0; } float sn = iter - log2(log2(dot(z, z))) + 4.0; float tcol = sn / max_iter; vec3 color = palette(tcol); gl_FragColor = vec4(color, 1.0); } `; let camera, scene, renderer, material; let center = new THREE.Vector2(0, 0); let scale = 2.5; let mouseX = 0.5; let paused = false; let clock = new THREE.Clock(); function init() { renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); scene = new THREE.Scene(); material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms: { u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_center: { value: center }, u_scale: { value: scale }, u_c: { value: new THREE.Vector2(0, 0) }, u_time: { value: 0 }, u_paused: { value: 0 } } }); const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); scene.add(quad); window.addEventListener('resize', onResize); window.addEventListener('wheel', onWheel); window.addEventListener('pointerdown', onPointerDown); window.addEventListener('pointermove', onPointerMove); window.addEventListener('pointerup', onPointerUp); window.addEventListener('keydown', onKeyDown); } function onResize() { renderer.setSize(window.innerWidth, window.innerHeight); material.uniforms.u_resolution.value.set(window.innerWidth, window.innerHeight); } let dragging = false; let lastPointer = new THREE.Vector2(); function onPointerDown(e) { dragging = true; lastPointer.set(e.clientX, e.clientY); } function onPointerMove(e) { mouseX = e.clientX / window.innerWidth; if (dragging) { const dx = (e.clientX - lastPointer.x) * scale / window.innerHeight; const dy = (e.clientY - lastPointer.y) * scale / window.innerHeight; center.x -= dx; center.y += dy; lastPointer.set(e.clientX, e.clientY); } } function onPointerUp() { dragging = false; } function onWheel(e) { const factor = e.deltaY > 0 ? 1.1 : 0.9; const mouse = new THREE.Vector2( (e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1 ); const pos = center.clone().add( new THREE.Vector2(mouse.x * scale, mouse.y * scale * window.innerHeight / window.innerWidth) ); center.lerp(pos, 1 - factor); scale *= factor; } function onKeyDown(e) { if (e.code === 'Space') { paused = !paused; material.uniforms.u_paused.value = paused ? 1 : 0; } } function animate() { requestAnimationFrame(animate); if (!paused) { material.uniforms.u_time.value = clock.getElapsedTime(); } material.uniforms.u_center.value.copy(center); material.uniforms.u_scale.value = scale; // mouse influences c const t = material.uniforms.u_time.value * 0.1; const radius = 0.7885 + 0.1 * (mouseX - 0.5); material.uniforms.u_c.value.set( radius * Math.cos(t), radius * Math.sin(t) ); renderer.render(scene, camera); } init(); animate(); </script> </body> </html> ```
**Action Plan:** **HTML Structure:** - Basic HTML5 structure with a single container div for the Three.js canvas. - Title and a brief description of the interactive fractal art piece. - Inline styles for the body and container div to ensure full-screen rendering. - CDN links for Three.js in script tags. **JavaScript Setup:** - Initialize a Three.js scene, camera, and renderer. - Create a full-screen quad (two triangles) using a PlaneGeometry and a ShaderMaterial. - Set up event listeners for mouse interactions (wheel, click, drag) and keyboard (space bar). - Implement zoom and pan functionality using the mouse wheel and drag events. - Update the Julia Set constant c based on the mouse's X position and time. - Toggle animations using the space bar. **GLSL Fragment Shader:** - Define uniform variables for resolution, time, zoom, pan offset, and the Julia Set constant c. - Implement the main rendering loop with the Julia Set algorithm. - Calculate the number of iterations before a point escapes and use it to determine the color from the smooth gradient palette. - Use the time uniform to animate the color palette. **Complete Code:** ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } #container { position: absolute; width: 100%; height: 100%; } </style> </head> <body> <div id="container"></div> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> <script> // Scene setup const scene = new THREE.Scene(); const camera = new THREE.Camera(); camera.position.z = 1; const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.getElementById('container').appendChild(renderer.domElement); // Shader material const material = new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, zoom: { value: 1 }, pan: { value: new THREE.Vector2(0, 0) }, c: { value: new THREE.Vector2(0, 0) }, colorShift: { value: 0 }, isPlaying: { value: true } }, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 resolution; uniform float time; uniform float zoom; uniform vec2 pan; uniform vec2 c; uniform float colorShift; uniform bool isPlaying; // Julia set fractal function vec3 juliaSet(vec2 uv, vec2 c) { float maxIterations = 100.0; float iteration = 0.0; vec2 z = uv; for (int i = 0; i < 100; i++) { if (i >= int(maxIterations)) { break; } float x = z.x * z.x - z.y * z.y + c.x; float y = 2.0 * z.x * z.y + c.y; z = vec2(x, y); if (dot(z, z) > 4.0) { break; } iteration += 1.0; } float normalizedIteration = iteration / maxIterations; return vec3(normalizedIteration); } // Color palette function vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) { return a + b * cos(6.28318 * (c * t + d)); } void main() { vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y; uv *= zoom; uv += pan; // Animate the Julia set constant c based on time and mouse X position float animatedC = isPlaying ? time * 0.1 : 0.0; vec2 animatedCVec = vec2(cos(animatedC), sin(animatedC)); animatedCVec = mix(c, animatedCVec, 0.5); vec3 color = juliaSet(uv, animatedCVec); // Color palette vec3 colorA = vec3(0.2, 0.0, 0.4); // Deep indigo vec3 colorB = vec3(0.8, 0.2, 0.6); // Magenta vec3 colorC = vec3(0.9, 0.5, 0.1); // Fiery orange vec3 colorD = vec3(0.9, 0.8, 0.2); // Golden yellow vec3 finalColor = palette(color.r + colorShift, colorA, colorB, colorC, colorD); gl_FragColor = vec4(finalColor, 1.0); } ` }); // Full-screen quad const geometry = new THREE.PlaneGeometry(2, 2); const quad = new THREE.Mesh(geometry, material); scene.add(quad); // Event listeners let isPlaying = true; let targetZoom = 1; let targetPan = new THREE.Vector2(0, 0); let mouseX = 0; window.addEventListener('wheel', (event) => { event.preventDefault(); targetZoom *= event.deltaY > 0 ? 0.9 : 1.1; }); window.addEventListener('mousedown', (event) => { event.preventDefault(); mouseX = event.clientX; }); window.addEventListener('mousemove', (event) => { if (event.buttons === 1) { const deltaX = event.clientX - mouseX; targetPan.x += deltaX / window.innerWidth; mouseX = event.clientX; } }); window.addEventListener('keydown', (event) => { if (event.code === 'Space') { isPlaying = !isPlaying; material.uniforms.isPlaying.value = isPlaying; } }); window.addEventListener('mousemove', (event) => { // Update the Julia set constant c based on the mouse's X position const mouseXNormalized = (event.clientX / window.innerWidth) * 2.0 - 1.0; material.uniforms.c.value.x = mouseXNormalized * 0.5; }); // Animation loop function animate() { requestAnimationFrame(animate); // Update uniforms material.uniforms.time.value += 0.01; material.uniforms.zoom.value += (targetZoom - material.uniforms.zoom.value) * 0.1; material.uniforms.pan.value.add(targetPan.clone().sub(material.uniforms.pan.value).multiplyScalar(0.1)); material.uniforms.colorShift.value = Math.sin(material.uniforms.time.value * 0.1) * 0.5; // Reset target pan targetPan.set(0, 0); renderer.render(scene, camera); } animate(); // Handle window resize window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); }); </script> </body> </html> ```
### Action Plan Before diving into the code, here's a detailed plan for creating this interactive, animated Julia Set fractal art piece as a self-contained HTML file. The goal is to produce a visually stunning, morphing fractal with smooth animations, custom coloring, and user interactions, all while keeping the core computation in a GLSL fragment shader for performance. #### 1. HTML Structure - The HTML will be a single, self-contained file starting with `<!DOCTYPE html>`. - In the `<head>`, include a `<script>` tag to load Three.js from a CDN (e.g., `https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js` for a stable version). - Use inline `<style>` to set the body margins to 0 and ensure the canvas fills the window. - The `<body>` will be empty initially, as the Three.js renderer will append a WebGL canvas dynamically. - The main logic will be in an inline `<script>` tag at the end of the `<body>`, containing all JavaScript and shader code. #### 2. JavaScript Setup with Three.js - **Scene Setup**: Create a Three.js scene, an orthographic camera (for 2D fractal rendering), and a WebGL renderer that resizes with the window. - **Geometry and Material**: Add a PlaneGeometry (e.g., size 2x2) to cover the full viewport. Use a ShaderMaterial with custom vertex and fragment shaders. The vertex shader will be simple (pass UV coordinates). The fragment shader will handle the Julia Set computation. - **Uniforms Management**: Define uniforms in the ShaderMaterial for: - `resolution` (vec2: window size for pixel calculations). - `time` (float: for animations, incremented in the loop unless paused). - `zoom` (float: starts at 1, modified by mouse wheel). - `center` (vec2: fractal coordinate center, updated for panning and zoom centering). - `mouse` (vec2: normalized mouse position, where X influences c's animation path). - `c` (vec2: the Julia Set constant, animated in JS). - `paused` (bool: toggles animations). - Additional params like `colorShift` (float: for pulsing colors). - **Animation Loop**: Use `requestAnimationFrame` to update uniforms (e.g., animate `c` on a circular path like `c = vec2(0.7885 * cos(theta), 0.7885 * sin(theta))` where theta evolves with time, but modulated by mouse.x to alter radius or angle offset). If not paused, increment time and colorShift. Render the scene each frame. - **Interactivity**: - **Zoom**: On mouse wheel event, adjust `zoom` factor (e.g., multiply/divide by 1.1), and update `center` to zoom towards the cursor's fractal position. - **Pan**: On mousedown, track start position; on mousemove, calculate delta and adjust `center`; on mouseup, stop panning. - **Morphing Control**: On mousemove, update `mouse` uniform. Use mouse.x to influence c's animation (e.g., mouse.x scales the radius of the circular path, making shapes change dynamically). - **Pause**: On keydown (space bar), toggle a `paused` flag, which stops incrementing time in the animation loop. - **Resize Handling**: Add a window resize event to update camera and renderer size, and pass new resolution to uniforms. - Keep JS efficient: All heavy computation (fractal iterations) is offloaded to the shader. #### 3. Logic for the GLSL Fragment Shader - **Uniform Variables**: As listed above (resolution, time, zoom, center, mouse, c, paused – though paused is handled in JS, time just doesn't update). - **Main Rendering Loop**: - Compute the fractal coordinate for the current fragment: Transform gl_FragCoord to normalized UV (-1 to 1), scale by zoom, offset by center, and aspect-correct using resolution. - Initialize complex z as the pixel position (vec2 zx = uv.x, zy = uv.y). - Iteration loop: For up to max_iter (e.g., 1000), compute new_zx = zx*zx - zy*zy + c.x; new_zy = 2*zx*zy + c.y; If (zx*zx + zy*zy > 4.0), break. Smooth the iteration value for better coloring (e.g., iter + log(log(|z|))/log(2)). - **Coloring**: Map the smoothed iteration (normalized 0-1) to a gradient. Define an array of vec3 colors: deep indigo (e.g., vec3(0.1,0.0,0.3)), magenta (vec3(1.0,0.0,1.0)), fiery orange (vec3(1.0,0.5,0.0)), golden yellow (vec3(1.0,0.8,0.0)). - Use a mix/lerp function to interpolate between colors based on the mapped value. - **Pulsing and Shifting**: Modulate the color mapping with a slow sin(time * 0.1) or similar to shift the gradient phases, creating a pulsing effect. Optionally, add subtle noise or secondary modulation for visual interest. - If the point doesn't escape (inside set), color it black or a base color. - Output to gl_FragColor. - The shader will be performant as it's per-pixel and GPU-accelerated. No JS-based fractal computation. This plan ensures a smooth, interactive experience. The animation will feel organic with the morphing c and pulsing colors, while interactions add engagement. Now, here's the complete HTML code implementing this. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Animated Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> </head> <body> <script> // Scene setup 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); // Geometry: full-screen plane const geometry = new THREE.PlaneGeometry(2, 2); // Shaders const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragmentShader = ` uniform vec2 resolution; uniform float time; uniform float zoom; uniform vec2 center; uniform vec2 mouse; uniform vec2 c; vec3 colorPalette(float t) { vec3 colors[4]; colors[0] = vec3(0.1, 0.0, 0.3); // deep indigo colors[1] = vec3(1.0, 0.0, 1.0); // magenta colors[2] = vec3(1.0, 0.5, 0.0); // fiery orange colors[3] = vec3(1.0, 0.8, 0.0); // golden yellow float shift = sin(time * 0.1) * 0.5 + 0.5; // slow pulse t = fract(t + shift); // shift gradient t *= 3.0; // for 4 colors int idx = int(floor(t)); float frac = fract(t); return mix(colors[idx], colors[(idx + 1) % 4], frac); } void main() { vec2 uv = (gl_FragCoord.xy / resolution.xy) * 2.0 - 1.0; uv.x *= resolution.x / resolution.y; // aspect correction uv = (uv / zoom) + center; vec2 z = uv; float iter = 0.0; const float max_iter = 1000.0; for (float i = 0.0; i < max_iter; i++) { vec2 z2 = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; if (dot(z2, z2) > 4.0) break; z = z2; iter = i; } if (iter >= max_iter) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // inside set } else { float smoothIter = iter + 1.0 - log(log(dot(z, z)) / 2.0) / log(2.0); float t = smoothIter / max_iter; t = pow(t, 0.5); // adjust curve for better visuals vec3 col = colorPalette(t); gl_FragColor = vec4(col, 1.0); } } `; // Material const material = new THREE.ShaderMaterial({ uniforms: { resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, time: { value: 0.0 }, zoom: { value: 1.0 }, center: { value: new THREE.Vector2(0.0, 0.0) }, mouse: { value: new THREE.Vector2(0.5, 0.5) }, c: { value: new THREE.Vector2(0.0, 0.0) } }, vertexShader, fragmentShader }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Animation and interaction variables let time = 0.0; let paused = false; let zoom = 1.0; let center = new THREE.Vector2(0.0, 0.0); let mouse = new THREE.Vector2(0.5, 0.5); let isDragging = false; let dragStart = new THREE.Vector2(); let dragStartCenter = new THREE.Vector2(); // Update uniforms function updateUniforms() { material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); material.uniforms.time.value = time; material.uniforms.zoom.value = zoom; material.uniforms.center.value.copy(center); material.uniforms.mouse.value.copy(mouse); // Animate c: circular path, radius modulated by mouse.x (0-1 -> radius 0.2 to 0.8) let radius = 0.2 + 0.6 * mouse.x; let theta = time * 0.1; // slow animation let cx = radius * Math.cos(theta); let cy = radius * Math.sin(theta); material.uniforms.c.value.set(cx, cy); } // Animation loop function animate() { requestAnimationFrame(animate); if (!paused) { time += 0.01; } updateUniforms(); renderer.render(scene, camera); } animate(); // Event listeners window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); camera.updateProjectionMatrix(); updateUniforms(); }); // Mouse move for morphing (and position) renderer.domElement.addEventListener('mousemove', (event) => { mouse.x = event.clientX / window.innerWidth; mouse.y = 1.0 - (event.clientY / window.innerHeight); // optional, but available if (isDragging) { let dx = (event.clientX - dragStart.x) / window.innerWidth * 2.0; let dy = (event.clientY - dragStart.y) / window.innerHeight * 2.0; center.x = dragStartCenter.x - dx / zoom * (window.innerWidth / window.innerHeight); center.y = dragStartCenter.y + dy / zoom; } }); // Pan: drag renderer.domElement.addEventListener('mousedown', (event) => { isDragging = true; dragStart.set(event.clientX, event.clientY); dragStartCenter.copy(center); }); renderer.domElement.addEventListener('mouseup', () => { isDragging = false; }); renderer.domElement.addEventListener('mouseleave', () => { isDragging = false; }); // Zoom: mouse wheel renderer.domElement.addEventListener('wheel', (event) => { event.preventDefault(); const zoomFactor = event.deltaY < 0 ? 1.1 : 1.0 / 1.1; const mouseX = (event.clientX / window.innerWidth) * 2 - 1; const mouseY = -(event.clientY / window.innerHeight) * 2 + 1; const aspect = window.innerWidth / window.innerHeight; // Fractal coord at mouse let fracX = (mouseX * aspect / zoom) + center.x; let fracY = (mouseY / zoom) + center.y; zoom *= zoomFactor; // Adjust center to zoom towards mouse center.x = fracX - (mouseX * aspect / zoom); center.y = fracY - (mouseY / zoom); }); // Pause: space bar window.addEventListener('keydown', (event) => { if (event.code === 'Space') { paused = !paused; } }); </script> </body> </html> ```
Here's my action plan for creating an interactive Julia Set fractal with Three.js and GLSL: HTML Structure: 1. Basic HTML5 boilerplate 2. Single full-screen canvas element 3. CDN links for Three.js 4. Inline GLSL shader code within script tags JavaScript Setup: 1. Initialize Three.js scene, camera, and renderer 2. Create a full-screen quad for fragment shader rendering 3. Set up uniforms for: - Fractal parameters (c constant, zoom, center point) - Color palette parameters - Time for animations - Mouse position/interaction 4. Implement mouse event handlers for: - Zoom (wheel) - Pan (drag) - Animation control (x-position) 5. Space bar toggle for animation pause GLSL Fragment Shader Logic: 1. Complex number operations for Julia Set calculation 2. Escape-time algorithm with smooth coloring 3. Dynamic c constant that follows a circular path (modified by mouse x) 4. Color gradient based on iteration count with time-based pulsing 5. Viewport transformations for zoom/pan centered on mouse Here's the complete implementation: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Julia Set Fractal</title> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script> // Scene setup const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Shader uniforms const uniforms = { u_time: { value: 0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_mouse: { value: new THREE.Vector2() }, u_zoom: { value: 1.0 }, u_center: { value: new THREE.Vector2(0, 0) }, u_paused: { value: false } }; // Shader material const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 u_resolution; uniform vec2 u_mouse; uniform vec2 u_center; uniform float u_time; uniform float u_zoom; uniform bool u_paused; const int MAX_ITER = 100; vec2 cMul(vec2 a, vec2 b) { return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x); } float julia(vec2 z, vec2 c) { int n = 0; for (int i = 0; i < MAX_ITER; i++) { z = cMul(z, z) + c; if (dot(z, z) > 4.0) break; n++; } if (n == MAX_ITER) return 0.0; return float(n) + 1.0 - log(log(length(z))/log(2.0)); } vec3 palette(float t) { vec3 a = vec3(0.5, 0.5, 0.5); vec3 b = vec3(0.5, 0.5, 0.5); vec3 c = vec3(1.0, 1.0, 1.0); vec3 d = vec3(0.263, 0.416, 0.557); float pulse = 0.5 + 0.5 * sin(u_time * 0.2); return a + b*cos(6.28318*(c*t + d + pulse*0.1)); } void main() { vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); uv *= u_zoom; uv += u_center; float time = u_paused ? 0.0 : u_time; float mouseInfluence = u_mouse.x / u_resolution.x * 2.0 - 1.0; vec2 c = vec2( 0.7885 * cos(time * 0.1 + mouseInfluence * 2.0), 0.7885 * sin(time * 0.15) ); float m = julia(uv, c); float t = m / float(MAX_ITER); vec3 col = palette(t); col = mix( mix( mix(vec3(0.08, 0.05, 0.3), vec3(0.7, 0.1, 0.5), smoothstep(0.0, 0.3, t)), vec3(1.0, 0.4, 0.1), smoothstep(0.3, 0.6, t) ), vec3(1.0, 0.9, 0.1), smoothstep(0.6, 1.0, t) ); col = pow(col, vec3(1.0/2.2)); gl_FragColor = vec4(col, 1.0); } ` }); // Full-screen quad const geometry = new THREE.PlaneGeometry(2, 2); const quad = new THREE.Mesh(geometry, material); scene.add(quad); // Mouse interaction let isDragging = false; let lastMouse = new THREE.Vector2(); let mouseDownPos = new THREE.Vector2(); window.addEventListener('mousedown', (e) => { isDragging = true; mouseDownPos.set(e.clientX, e.clientY); lastMouse.set(e.clientX, e.clientY); }); window.addEventListener('mouseup', () => { isDragging = false; }); window.addEventListener('mousemove', (e) => { uniforms.u_mouse.value.set(e.clientX, e_clientY); if (isDragging) { const deltaX = (e.clientX - lastMouse.x) / window.innerWidth; const deltaY = (e.clientY - lastMouse.y) / window.innerHeight; uniforms.u_center.value.x -= deltaX * 2.0 / uniforms.u_zoom.value; uniforms.u_center.value.y += deltaY * 2.0 / uniforms.u_zoom.value; lastMouse.set(e.clientX, e.clientY); } }); window.addEventListener('wheel', (e) => { const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9; uniforms.u_zoom.value *= zoomFactor; // Zoom toward mouse position const mouseNDC = new THREE.Vector2( (e.clientX / window.innerWidth) * 2.0 - 1.0, -(e.clientY / window.innerHeight) * 2.0 + 1.0 ); const zoomCenter = new THREE.Vector2( mouseNDC.x / min(window.innerWidth, window.innerHeight) * 2.0, mouseNDC.y / min(window.innerWidth, window.innerHeight) * 2.0 ); uniforms.u_center.value.x += zoomCenter.x * (1.0 - zoomFactor) / uniforms.u_zoom.value; uniforms.u_center.value.y += zoomCenter.y * (1.0 - zoomFactor) / uniforms.u_zoom.value; }); // Pause animation window.addEventListener('keydown', (e) => { if (e.code === 'Space') { uniforms.u_paused.value = !uniforms.u_paused.value; e.preventDefault(); } }); // Handle resize window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.set(window.innerWidth, window.innerHeight); }); // Animation loop function animate() { requestAnimationFrame(animate); if (!uniforms.u_paused.value) { uniforms.u_time.value += 0.016; // Roughly 60fps } renderer.render(scene, camera); } animate(); </script> </body> </html> ``` This implementation creates a continuously morphing Julia Set fractal with: 1. Smooth color transitions through indigo, magenta, orange, and yellow 2. Mouse-controlled zoom and pan functionality 3. Mouse x-position influencing the fractal's morphing path 4. Space bar to pause/resume animations 5. Full-screen responsive rendering The fractal is calculated entirely in the fragment shader using complex number operations and escape-time algorithm with smooth coloring. The uniforms handle all the interactive parameters and time-based animations.
## Plan for Creating an Interactive Julia Set Fractal Art Piece ### Overview To create a visually stunning, interactive Julia Set fractal art piece, we will structure our work into three main components: HTML setup, JavaScript code using Three.js, and a GLSL fragment shader for rendering the fractal. ### HTML Setup The HTML file will contain: - A script tag loading Three.js from a CDN. - A script tag for our custom JavaScript code. - A basic structure for displaying the Three.js renderer output. ### JavaScript Setup with Three.js 1. **Initialization**: - Create a Three.js scene, camera, and renderer. - Set up the renderer to fill the window and handle resizing. - Add event listeners for mouse wheel (zoom), mouse down and move (pan), and key presses (pause animation). 2. **Plane Geometry and Shader Material**: - Create a plane geometry that will be used to display the fractal. - Create a ShaderMaterial with our custom GLSL vertex and fragment shaders. - The vertex shader will be straightforward, passing through vertex positions and UVs. - The fragment shader will contain the logic for rendering the Julia Set. 3. **Uniforms and Animation**: - Define uniforms for the fragment shader, including: - `c`: The complex constant defining the Julia Set, animated over time. - `time`: For pulsing and shifting colors. - `resolution`: To adjust rendering based on canvas size. - `zoom`: To adjust the zoom level based on user input. - `offset`: To adjust the view based on panning. - `mouseInfluence`: To adjust the animation path of `c` based on mouse X position. - Use the `requestAnimationFrame` loop to update uniforms and render the scene. ### GLSL Fragment Shader Logic 1. **Uniforms**: - Receive the uniforms defined in the JavaScript code, including `c`, `time`, `resolution`, `zoom`, `offset`, and `mouseInfluence`. 2. **Julia Set Rendering**: - Iterate over the complex plane for each pixel, applying the Julia Set formula with the current `c` value. - Color pixels based on the number of iterations before escaping the set. 3. **Coloring**: - Use a smooth gradient ("deep indigo -> magenta -> fiery orange -> golden yellow") based on the iteration count. - Animate the color palette by shifting its base colors over `time`. 4. **Morphing and Animation**: - Update `c` in the JavaScript code to follow a circular path in the complex plane, influenced by `mouseInfluence` for altering the path based on mouse X position. ### Interactivity Implementation 1. **Zoom and Pan**: - Adjust the `zoom` and `offset` uniforms based on mouse wheel and mouse drag events. 2. **Animation Control**: - Update the animation of `c` and color shifting based on the `time` uniform, and pause or resume this update based on a key press event (space bar). 3. **Mouse Influence on Animation**: - Update the `mouseInfluence` uniform based on the mouse's X position relative to the window's width. By structuring the project in this way, we will create an interactive and visually stunning Julia Set fractal art piece that morphs and evolves over time, influenced by user interaction. Let's proceed to implement this plan. Here's a complete code example in a single HTML file: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Julia Set Fractal</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 scene = new THREE.Scene(); let camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); let renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); let geometry = new THREE.PlaneGeometry(2, 2); let uniforms = { c: { value: new THREE.Vector2(0.285, 0.01) }, time: { value: 0 }, resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, zoom: { value: 1 }, offset: { value: new THREE.Vector2(0, 0) }, mouseInfluence: { value: 0 }, paused: { value: false } }; let material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 c; uniform float time; uniform vec2 resolution; uniform float zoom; uniform vec2 offset; uniform float mouseInfluence; uniform bool paused; void main() { vec2 uv = (gl_FragCoord.xy - resolution / 2.0) * zoom + offset; uv /= resolution.y; vec2 z = uv; float iter = 0.0; const float max_iter = 256.0; if (!paused) { vec2 anim_c = c + 0.1 * vec2(cos(time + mouseInfluence), sin(time + mouseInfluence)); for (float i = 0.0; i < max_iter; i++) { z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + anim_c; if (dot(z, z) > 4.0) { iter = i; break; } } } vec3 col = vec3(0); if (iter > 0.0) { float t = iter / max_iter; col = 0.5 + 0.5 * cos(3.0 + t * 15.0 + time + vec3(0, 0.6, 1.0)); col = mix(vec3(0.1, 0.1, 0.3), vec3(1, 0.8, 0.2), col); } gl_FragColor = vec4(col, 1.0); } ` }); let plane = new THREE.Mesh(geometry, material); scene.add(plane); let mouseDown = false; let lastX, lastY; document.addEventListener('mousedown', (e) => { mouseDown = true; lastX = e.clientX; lastY = e.clientY; }); document.addEventListener('mousemove', (e) => { if (mouseDown) { let dx = e.clientX - lastX; let dy = e.clientY - lastY; uniforms.offset.value.x += dx / window.innerHeight * uniforms.zoom.value; uniforms.offset.value.y -= dy / window.innerHeight * uniforms.zoom.value; lastX = e.clientX; lastY = e.clientY; } uniforms.mouseInfluence.value = (e.clientX / window.innerWidth) * 2.0 - 1.0; }); document.addEventListener('mouseup', () => { mouseDown = false; }); document.addEventListener('wheel', (e) => { let scale = 1.1; if (e.deltaY < 0) scale = 1 / scale; let mouse = new THREE.Vector2(e.clientX, e.clientY); let before = new THREE.Vector2( (mouse.x - window.innerWidth / 2) / window.innerHeight * uniforms.zoom.value + uniforms.offset.value.x, (mouse.y - window.innerHeight / 2) / window.innerHeight * uniforms.zoom.value - uniforms.offset.value.y ); uniforms.zoom.value *= scale; let after = new THREE.Vector2( (mouse.x - window.innerWidth / 2) / window.innerHeight * uniforms.zoom.value + uniforms.offset.value.x, (mouse.y - window.innerHeight / 2) / window.innerHeight * uniforms.zoom.value - uniforms.offset.value.y ); uniforms.offset.value.x -= after.x - before.x; uniforms.offset.value.y += after.y - before.y; }); document.addEventListener('keydown', (e) => { if (e.key === ' ') { uniforms.paused.value = !uniforms.paused.value; } }); function animate() { requestAnimationFrame(animate); if (!uniforms.paused.value) { uniforms.time.value += 0.01; uniforms.c.value.x = 0.285 + 0.01 * Math.cos(uniforms.time.value); uniforms.c.value.y = 0.01 + 0.01 * Math.sin(uniforms.time.value); } renderer.render(scene, camera); } window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.resolution.value.set(window.innerWidth, window.innerHeight); }); animate(); </script> </body> </html> ``` This HTML file creates an interactive Julia Set fractal visualization with the described features and functionalities. The GLSL fragment shader handles the rendering of the Julia Set with animated complex constant `c` and color palette. JavaScript code manages user interactions, updates uniforms, and handles rendering.
## Action Plan ### HTML Structure - Single HTML file with embedded CSS and JavaScript - Canvas element for Three.js rendering - Event listeners for mouse interactions and keyboard controls ### JavaScript Setup with Three.js - Initialize Three.js scene with orthographic camera for 2D fractal rendering - Create a full-screen quad geometry with a custom shader material - Implement mouse event handlers for: - Wheel events for zooming (centered on cursor position) - Mouse down/move/up for panning - Mouse move for morphing control - Spacebar for pause/play toggle - Animation loop to update time-based uniforms ### GLSL Fragment Shader Logic **Uniform Variables:** - `u_time`: Current time for animations - `u_resolution`: Screen resolution - `u_zoom`: Current zoom level - `u_offset`: Pan offset (x, y) - `u_mouseInfluence`: Mouse X position influence on morphing - `u_paused`: Boolean for pause state **Main Rendering Logic:** 1. Convert screen coordinates to complex plane coordinates 2. Apply zoom and pan transformations 3. Calculate animated Julia Set constant `c` using circular path + mouse influence 4. Implement Julia Set iteration algorithm 5. Apply smooth color gradient based on iteration count and time 6. Return final color with time-based color shifting Let me implement this plan: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Julia Set Fractal</title> <style> body { margin: 0; padding: 0; overflow: hidden; background: #000; font-family: Arial, sans-serif; } canvas { display: block; cursor: grab; } canvas:active { cursor: grabbing; } .controls { position: absolute; top: 20px; left: 20px; color: white; font-size: 14px; background: rgba(0, 0, 0, 0.7); padding: 15px; border-radius: 8px; max-width: 300px; } .controls h3 { margin: 0 0 10px 0; color: #ff6b35; } .controls p { margin: 5px 0; line-height: 1.4; } </style> </head> <body> <div class="controls"> <h3>Julia Set Fractal Explorer</h3> <p><strong>Mouse Wheel:</strong> Zoom in/out</p> <p><strong>Click & Drag:</strong> Pan view</p> <p><strong>Mouse X:</strong> Morph fractal shape</p> <p><strong>Spacebar:</strong> Pause/Resume animation</p> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Global variables let scene, camera, renderer, material, mesh; let mouse = { x: 0, y: 0 }; let zoom = 1.0; let offset = { x: 0, y: 0 }; let isDragging = false; let dragStart = { x: 0, y: 0 }; let offsetStart = { x: 0, y: 0 }; let isPaused = false; let startTime = Date.now(); let pausedTime = 0; // Vertex shader (simple pass-through) const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; // Fragment shader with Julia Set implementation const fragmentShader = ` uniform vec2 u_resolution; uniform float u_time; uniform float u_zoom; uniform vec2 u_offset; uniform float u_mouseInfluence; uniform bool u_paused; varying vec2 vUv; // Color palette function vec3 getColor(float t, float timeShift) { t = fract(t + timeShift); vec3 indigo = vec3(0.2, 0.0, 0.4); vec3 magenta = vec3(0.8, 0.2, 0.8); vec3 orange = vec3(1.0, 0.4, 0.1); vec3 yellow = vec3(1.0, 0.9, 0.2); if (t < 0.33) { return mix(indigo, magenta, t * 3.0); } else if (t < 0.66) { return mix(magenta, orange, (t - 0.33) * 3.0); } else { return mix(orange, yellow, (t - 0.66) * 3.0); } } void main() { vec2 uv = (vUv - 0.5) * 2.0; uv.x *= u_resolution.x / u_resolution.y; // Apply zoom and offset vec2 z = uv / u_zoom + u_offset; // Animated Julia Set constant with mouse influence float animTime = u_paused ? 0.0 : u_time * 0.3; float radius = 0.7885 + u_mouseInfluence * 0.2; float angle = animTime + u_mouseInfluence * 3.14159; vec2 c = vec2(cos(angle), sin(angle)) * radius; // Julia Set iteration const int maxIterations = 100; int iterations = 0; float bailout = 4.0; for (int i = 0; i < maxIterations; i++) { if (dot(z, z) > bailout) break; // z = z^2 + c float x = z.x * z.x - z.y * z.y + c.x; float y = 2.0 * z.x * z.y + c.y; z = vec2(x, y); iterations++; } // Smooth coloring float smoothIter = float(iterations); if (iterations < maxIterations) { smoothIter += 1.0 - log2(log2(dot(z, z))); } // Normalize and apply color float t = smoothIter / float(maxIterations); if (iterations >= maxIterations) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { float colorShift = u_paused ? 0.0 : u_time * 0.1; vec3 color = getColor(t * 3.0, colorShift); // Add some brightness variation float brightness = 0.8 + 0.4 * sin(u_time * 0.5 + t * 10.0); color *= brightness; gl_FragColor = vec4(color, 1.0); } } `; function init() { // Create scene scene = new THREE.Scene(); // Create camera camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); // Create renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Create shader material material = new THREE.ShaderMaterial({ vertexShader: vertexShader, fragmentShader: fragmentShader, uniforms: { u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_time: { value: 0.0 }, u_zoom: { value: zoom }, u_offset: { value: new THREE.Vector2(offset.x, offset.y) }, u_mouseInfluence: { value: 0.0 }, u_paused: { value: false } } }); // Create geometry and mesh const geometry = new THREE.PlaneGeometry(2, 2); mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Add event listeners setupEventListeners(); // Start animation loop animate(); } function setupEventListeners() { const canvas = renderer.domElement; // Mouse move for morphing control window.addEventListener('mousemove', (event) => { mouse.x = event.clientX; mouse.y = event.clientY; // Convert mouse X to influence value (-1 to 1) const mouseInfluence = (event.clientX / window.innerWidth - 0.5) * 2; material.uniforms.u_mouseInfluence.value = mouseInfluence; // Handle dragging if (isDragging) { const deltaX = (event.clientX - dragStart.x) / window.innerWidth * 4 / zoom; const deltaY = -(event.clientY - dragStart.y) / window.innerHeight * 4 / zoom; offset.x = offsetStart.x - deltaX; offset.y = offsetStart.y - deltaY; material.uniforms.u_offset.value.set(offset.x, offset.y); } }); // Mouse down canvas.addEventListener('mousedown', (event) => { isDragging = true; dragStart.x = event.clientX; dragStart.y = event.clientY; offsetStart.x = offset.x; offsetStart.y = offset.y; }); // Mouse up window.addEventListener('mouseup', () => { isDragging = false; }); // Mouse wheel for zooming canvas.addEventListener('wheel', (event) => { event.preventDefault(); const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1; const newZoom = zoom * zoomFactor; // Get mouse position in normalized coordinates const rect = canvas.getBoundingClientRect(); const mouseX = (event.clientX - rect.left) / rect.width; const mouseY = (event.clientY - rect.top) / rect.height; // Convert to world coordinates const worldX = (mouseX - 0.5) * 2 * (window.innerWidth / window.innerHeight) / zoom + offset.x; const worldY = -(mouseY - 0.5) * 2 / zoom + offset.y; // Update zoom and adjust offset to zoom towards mouse zoom = newZoom; offset.x = worldX - (mouseX - 0.5) * 2 * (window.innerWidth / window.innerHeight) / zoom; offset.y = worldY + (mouseY - 0.5) * 2 / zoom; material.uniforms.u_zoom.value = zoom; material.uniforms.u_offset.value.set(offset.x, offset.y); }); // Spacebar for pause/play window.addEventListener('keydown', (event) => { if (event.code === 'Space') { event.preventDefault(); isPaused = !isPaused; material.uniforms.u_paused.value = isPaused; if (isPaused) { pausedTime = Date.now() - startTime; } else { startTime = Date.now() - pausedTime; } } }); // Window resize window.addEventListener('resize', () => { const width = window.innerWidth; const height = window.innerHeight; renderer.setSize(width, height); material.uniforms.u_resolution.value.set(width, height); }); } function animate() { requestAnimationFrame(animate); // Update time uniform const currentTime = isPaused ? pausedTime : Date.now() - startTime; material.uniforms.u_time.value = currentTime * 0.001; renderer.render(scene, camera); } // Initialize when page loads init(); </script> </body> </html> ``` This implementation creates a stunning, interactive Julia Set fractal with all the requested features: **Technical Implementation:** - Uses Three.js with GLSL fragment shader for core fractal rendering - Julia Set algorithm implemented entirely in the fragment shader - Smooth color transitions through the specified palette - Real-time animation and morphing capabilities **Interactive Features:** - **Zoom:** Mouse wheel zooms in/out centered on cursor position - **Pan:** Click and drag to pan the view - **Morphing:** Mouse X position influences the Julia Set constant animation path - **Pause/Play:** Spacebar toggles all animations - **Responsive:** Adapts to window resizing **Visual Excellence:** - Smooth color gradient from deep indigo → magenta → orange → yellow - Time-based color shifting and brightness pulsing - Anti-aliased rendering with smooth iteration coloring - Dark background for high contrast The fractal continuously morphs through different Julia Set shapes while maintaining smooth, mesmerizing animations that respond beautifully to user interaction.
Action Plan ============ 1. HTML Structure • A minimal, standards-compliant HTML5 document. • One `<canvas>` generated automatically by Three.js – no other DOM elements are needed. • The Three.js library is loaded from a CDN. • All logic is inside a single `<script>` tag so the file is entirely self-contained. 2. JavaScript / Three.js Setup • Create a WebGLRenderer, Scene, and an OrthographicCamera that spans clip-space (–1 … 1). • Add a single full-screen plane (two triangles) that uses a ShaderMaterial. • Supply the shader with the following uniforms: – u_resolution (vec2) : current canvas size in pixels – u_time (float) : elapsed time in seconds (frozen when paused) – u_zoom (float) : current zoom level (1.0 = default) – u_offset (vec2) : pan offset in the complex plane – u_c (vec2) : the animated Julia constant – u_paletteShift (float) : slow global hue shift for the gradient • The render loop (`requestAnimationFrame`) updates these uniforms each frame unless playback is paused. 3. Interaction Logic • Zoom (mouse-wheel): – Convert cursor position from screen to world coordinates before and after the zoom step. – Adjust `u_zoom` and compensate `u_offset` so the zoom is centred on the cursor. • Pan (mouse drag): – On mousedown, remember the initial world coordinate under the cursor. – On mousemove, update `u_offset` so the dragged point stays under the cursor. • Morphing control (mouse X): – `mouseX` in the range [–1 … 1] influences the angular speed of the animated constant `c(t)`. • Pause (`space` key): toggles a `playing` boolean; when false, `u_time` stops advancing and the shader receives a frozen value. 4. GLSL Fragment Shader • Convert fragment coordinates to complex-plane coordinates: `vec2 z = (frag - 0.5*res) / res.y * u_zoom + u_offset;` • Iterative loop (max 200) for `z = z² + c`; compute smooth iteration count to avoid banding. • Colouring: – Normalise the smooth iteration count to [0 … 1]. – Add `u_paletteShift` and take the fractional part to create a slow colour pulse. – Map the result through a custom gradient: deep-indigo → magenta → fiery-orange → golden-yellow. – Points that never escape (inside the Julia set) are rendered nearly black. 5. Result • A single HTML file that, when opened in any modern browser, shows a living, morphing Julia fractal that can be zoomed and panned fluidly, whose colours breathe over time, and whose evolution can be controlled and paused by the user. ------------------------------------------------ Below is the complete, self-contained implementation: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Interactive Morphing Julia Fractal – Three.js + GLSL</title> <style> html,body{margin:0;height:100%;overflow:hidden;background:#000;} canvas{display:block;} </style> <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js"></script> </head> <body> <script> // ----------------------------------------------------------- // Global variables // ----------------------------------------------------------- let scene, camera, renderer, material, startClock; let playing = true; let zoom = 1.0; let offset = new THREE.Vector2(0.0, 0.0); let mouse = {x:0, y:0}; let isDragging = false; let dragStartWorld = new THREE.Vector2(); const MAX_ITER = 200; // ----------------------------------------------------------- // Initialise Three.js // ----------------------------------------------------------- init(); animate(); function init(){ scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1); renderer= new THREE.WebGLRenderer({antialias:false}); document.body.appendChild(renderer.domElement); onResize(); window.addEventListener('resize', onResize); const vertex = ` varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position,1.0); }`; const fragment = ` precision highp float; #define MAX_ITER ${MAX_ITER} uniform vec2 u_resolution; uniform float u_time; uniform float u_zoom; uniform vec2 u_offset; uniform vec2 u_c; uniform float u_paletteShift; varying vec2 vUv; // 4-colour gradient helper vec3 gradient(float t){ vec3 c1 = vec3(0.10, 0.00, 0.30); // deep indigo vec3 c2 = vec3(0.80, 0.00, 0.60); // magenta vec3 c3 = vec3(1.00, 0.30, 0.00); // fiery orange vec3 c4 = vec3(1.00, 0.80, 0.00); // golden yellow if(t < 0.333){ return mix(c1,c2, t/0.333); }else if(t < 0.666){ return mix(c2,c3, (t-0.333)/0.333); }else{ return mix(c3,c4, (t-0.666)/0.334); } } void main(){ // Convert fragment to complex plane vec2 frag = gl_FragCoord.xy; vec2 z = (frag - 0.5 * u_resolution) / u_resolution.y; // maintain aspect z = z * u_zoom + u_offset; vec2 c = u_c; float i; float smooth=0.0; for(i=0.0;i<float(MAX_ITER);i++){ // z = z^2 + c vec2 z2 = vec2( z.x*z.x - z.y*z.y, 2.0*z.x*z.y ) + c; z = z2; if(dot(z,z) > 4.0){ // smooth iteration count float log_zn = log(dot(z,z))/2.0; float nu = log(log_zn / log(2.0)) / log(2.0); smooth = i + 1.0 - nu; break; } } if(i >= float(MAX_ITER)){ // inside set gl_FragColor = vec4(0.0,0.0,0.0,1.0); }else{ float t = smooth / float(MAX_ITER); t = fract(t + u_paletteShift); // animate colours vec3 col = gradient(t); gl_FragColor = vec4(col,1.0); } }`; material = new THREE.ShaderMaterial({ vertexShader : vertex, fragmentShader: fragment, uniforms : { u_resolution : {value: new THREE.Vector2()}, u_time : {value: 0}, u_zoom : {value: zoom}, u_offset : {value: offset.clone()}, u_c : {value: new THREE.Vector2()}, u_paletteShift: {value: 0} } }); const quad = new THREE.Mesh(new THREE.PlaneGeometry(2,2), material); scene.add(quad); startClock = new THREE.Clock(); // Interaction events window.addEventListener('wheel', onWheel, {passive:false}); window.addEventListener('mousedown',onDown); window.addEventListener('mousemove',onMove); window.addEventListener('mouseup', ()=>{isDragging=false;}); window.addEventListener('keydown', onKey); window.addEventListener('mouseleave',()=>{isDragging=false;}); } function onResize(){ const w = window.innerWidth; const h = window.innerHeight; renderer.setSize(w,h); material.uniforms.u_resolution.value.set(w,h); } function animate(){ requestAnimationFrame(animate); if(playing){ material.uniforms.u_time.value = startClock.getElapsedTime(); material.uniforms.u_paletteShift.value = (material.uniforms.u_time.value*0.05) % 1.0; } // Animate complex constant c along a slow circular path const t = material.uniforms.u_time.value; const speed = 0.2 + 0.3 * mouse.x; // mouse X modulates speed/direction const angle = t * speed; const radius = 0.7885; // classical interesting radius material.uniforms.u_c.value.set( radius * Math.cos(angle), radius * Math.sin(angle) ); // Push zoom & offset material.uniforms.u_zoom.value = zoom; material.uniforms.u_offset.value = offset; renderer.render(scene,camera); } // ----------------------------------------------------------- // Interaction handlers // ----------------------------------------------------------- function screenToWorld(px,py){ const res = material.uniforms.u_resolution.value; const ndc = new THREE.Vector2( (px - res.x*0.5) / res.y, (py - res.y*0.5) / res.y ); return new THREE.Vector2( ndc.x * zoom + offset.x, -ndc.y * zoom + offset.y // negate Y because screen Y is down ); } function onWheel(e){ e.preventDefault(); const delta = Math.sign(e.deltaY); const zoomFactor = (delta>0) ? 1.1 : 0.9; const before = screenToWorld(e.clientX, e.clientY); zoom *= zoomFactor; const after = screenToWorld(e.clientX, e.clientY); // Re-centre so the point under cursor stays fixed offset.add(before).sub(after); } function onDown(e){ isDragging = true; dragStartWorld.copy( screenToWorld(e.clientX, e.clientY) ); } function onMove(e){ // Update mouse X normalised const res = material.uniforms.u_resolution.value; mouse.x = (e.clientX / res.x) * 2.0 - 1.0; // [-1,1] mouse.y = (e.clientY / res.y) * 2.0 - 1.0; if(isDragging){ const currentWorld = screenToWorld(e.clientX, e.clientY); const diff = new THREE.Vector2().subVectors(dragStartWorld, currentWorld); offset.add(diff); } } function onKey(e){ if(e.code === 'Space'){ playing = !playing; if(playing) startClock.start(); else startClock.stop(); } } </script> </body> </html> ```
### Plan for Creating Interactive Julia Set Fractal Art with Three.js and GLSL #### Overview The goal is to create a visually captivating, interactive Julia Set fractal artwork using Three.js for rendering and GLSL for the core fractal logic. The piece will feature smooth animations, dynamic color transitions, and user interactions like zooming, panning, and morphing control. #### HTML Structure - **Basic Setup**: Include a full HTML document with a `<head>` section for metadata and external resources, and a `<body>` section for the canvas. - **Three.js CDN**: Load Three.js from a CDN (e.g., jsDelivr) using a `<script>` tag to handle 3D rendering and scene management. - **Inline Code**: Embed all JavaScript and shader code within the HTML file using `<script>` tags to ensure it is self-contained. #### JavaScript Setup with Three.js - **Scene Initialization**: - Create a Three.js `Scene`, `Camera` (Orthographic for 2D fractal rendering), and `WebGLRenderer`. - Set up a full-screen canvas that resizes with the window. - **Geometry and Material**: - Use a `PlaneGeometry` to cover the screen, acting as a canvas for the fractal. - Define a `ShaderMaterial` with custom vertex and fragment shaders. The vertex shader will be minimal (passing UV coordinates), while the fragment shader will handle fractal computation. - **Uniforms for Shader**: - `u_time`: Float for animation timing (morphing of Julia Set constant `c` and color pulsing). - `u_resolution`: Vector2 for screen dimensions to handle aspect ratio. - `u_zoom`: Float for zoom level. - `u_offset`: Vector2 for panning the fractal view. - `u_c`: Vector2 representing the Julia Set constant (real and imaginary parts), animated over time and influenced by mouse X position. - `u_colorShift`: Float for animating color transitions. - **Animation Loop**: - Update `u_time` for continuous animation of `c` and colors. - Animate `c` in a circular path in the complex plane, modified by mouse X position for morphing control. - Toggle animation updates based on a play/pause state controlled by the space bar. - **Interactivity**: - **Zoom**: Use mouse wheel events to adjust `u_zoom`, centered on cursor position by updating `u_offset`. - **Pan**: Track mouse down/move/up events to update `u_offset` for dragging the view. - **Morphing**: Map mouse X position to influence the radius or angle of `c`’s circular path. - **Pause/Play**: Listen for space bar keypress to toggle a boolean controlling animation updates. #### GLSL Fragment Shader Logic - **Uniform Variables**: - Receive all uniforms defined in JavaScript (`u_time`, `u_resolution`, `u_zoom`, `u_offset`, `u_c`, `u_colorShift`). - **Fractal Computation**: - Map pixel coordinates (UV) to the complex plane, adjusted by `u_zoom` and `u_offset` for navigation. - Implement the Julia Set iteration: for a pixel’s complex coordinate `z`, iterate `z = z*z + c` until escape (`|z|` exceeds a threshold, e.g., 2.0) or max iterations reached. - Track the iteration count for coloring. - **Coloring**: - Use iteration count to determine escape time, applying a smooth gradient effect. - Define a gradient from deep indigo -> magenta -> fiery orange -> golden yellow using HSV-to-RGB conversion or direct color interpolation in RGB space, shifted by `u_colorShift` for pulsing. - Apply a smoothstep or logarithmic scaling to the iteration count for smoother color transitions. - **Rendering**: - Output the final color as `gl_FragColor` with alpha set to 1.0 for opacity. #### Creative Considerations - **Animation**: Ensure `c`’s circular path is slow and smooth (e.g., using `sin` and `cos` with a small time multiplier), with mouse X subtly altering the radius or speed for organic morphing. - **Colors**: Implement a time-based offset in the color gradient to create a pulsing, breathing effect, synchronized across the palette for harmony. - **Interactivity**: Balance zoom and pan sensitivity for intuitive control, ensuring the fractal remains centered under the cursor during zoom. With this plan, the implementation will deliver a mesmerizing, interactive Julia Set fractal with dynamic visuals and user-driven exploration. ### Implementation ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Julia Set Fractal</title> <script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.155.0/build/three.esm.js", "three/addons/": "https://unpkg.com/three@0.155.0/examples/jsm/" } } </script> <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); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Plane geometry for full-screen shader const geometry = new THREE.PlaneGeometry(2, 2); const uniforms = { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_zoom: { value: 1.0 }, u_offset: { value: new THREE.Vector2(0.0, 0.0) }, u_c: { value: new THREE.Vector2(0.0, 0.0) }, u_colorShift: { value: 0.0 } }; // Shader material const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float u_time; uniform vec2 u_resolution; uniform float u_zoom; uniform vec2 u_offset; uniform vec2 u_c; uniform float u_colorShift; varying vec2 vUv; 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() { vec2 uv = (vUv - 0.5) * 2.0; uv.x *= u_resolution.x / u_resolution.y; vec2 z = (uv / u_zoom) + u_offset; vec2 c = u_c; float iterations = 0.0; const float maxIter = 100.0; float escaped = 0.0; for (float i = 0.0; i < maxIter; i++) { z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; if (length(z) > 2.0) { escaped = 1.0; iterations = i; break; } } vec3 color = vec3(0.0); if (escaped > 0.5) { float smoothIter = iterations + 1.0 - log2(log2(length(z))); float t = (smoothIter / maxIter) * 2.0 + u_colorShift; float hue = mod(t, 1.0); color = hsv2rgb(vec3(hue, 0.8, 0.9)); } else { color = vec3(0.0, 0.0, 0.1); } gl_FragColor = vec4(color, 1.0); } ` }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Interaction variables let isPlaying = true; let mousePos = new THREE.Vector2(0, 0); let isDragging = false; let dragStart = new THREE.Vector2(0, 0); let offsetStart = new THREE.Vector2(0, 0); // Event listeners window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.set(window.innerWidth, window.innerHeight); }); window.addEventListener('mousemove', (e) => { mousePos.set( (e.clientX / window.innerWidth) * 2 - 1, 1 - (e.clientY / window.innerHeight) * 2 ); if (isDragging) { let delta = new THREE.Vector2( ((e.clientX - dragStart.x) / window.innerWidth) * 2, ((dragStart.y - e.clientY) / window.innerHeight) * 2 ); delta.multiplyScalar(2.0 / uniforms.u_zoom.value); uniforms.u_offset.value.copy(offsetStart).add(delta); } }); window.addEventListener('mousedown', (e) => { isDragging = true; dragStart.set(e.clientX, e.clientY); offsetStart.copy(uniforms.u_offset.value); }); window.addEventListener('mouseup', () => { isDragging = false; }); window.addEventListener('mouseleave', () => { isDragging = false; }); window.addEventListener('wheel', (e) => { e.preventDefault(); let zoomDelta = -Math.sign(e.deltaY) * 0.1; let oldZoom = uniforms.u_zoom.value; let newZoom = Math.max(0.1, oldZoom + zoomDelta * oldZoom); uniforms.u_zoom.value = newZoom; let mouseWorld = new THREE.Vector2( (mousePos.x * (window.innerWidth / window.innerHeight)), mousePos.y ); let offsetDelta = mouseWorld.multiplyScalar((oldZoom - newZoom) / (oldZoom * newZoom)); uniforms.u_offset.value.add(offsetDelta); }, { passive: false }); window.addEventListener('keydown', (e) => { if (e.code === 'Space') { isPlaying = !isPlaying; e.preventDefault(); } }); // Animation loop let time = 0; function animate() { requestAnimationFrame(animate); if (isPlaying) { time += 0.005; uniforms.u_time.value = time; // Animate Julia constant c in a circular path, influenced by mouse X let baseRadius = 0.7885; let mouseInfluence = (mousePos.x * 0.5 + 0.5) * 0.2 + 0.9; // 0.9 to 1.1 let angle = time * 0.3; uniforms.u_c.value.set( baseRadius * mouseInfluence * Math.cos(angle), baseRadius * mouseInfluence * Math.sin(angle) ); // Animate color shift uniforms.u_colorShift.value = Math.sin(time * 0.2) * 0.5; } renderer.render(scene, camera); } animate(); </script> </body> </html> ```
Of course. Here is a plan and the corresponding self-contained HTML file for the interactive, animated Julia Set fractal. ### Action Plan #### 1. HTML Structure The foundation will be a single HTML file. - **`<head>`**: Will contain a `<title>`, basic meta tags, and a `<style>` block to make the canvas fill the entire viewport without scrollbars. - **`<body>`**: Will hold the main `<script>` for our application logic. - We'll include the Three.js library from a CDN (`https://unpkg.com/three`). - The GLSL fragment shader code will be embedded within a `<script>` tag with a custom type (`type="x-shader/x-fragment"`) for clean separation and easy retrieval in JavaScript. - A `<div>` element with an `id` (e.g., `container`) will serve as the mount point for the Three.js renderer's canvas. #### 2. JavaScript & Three.js Setup This script will orchestrate the scene, interactivity, and communication with the shader. - **Scene Setup**: Initialize a `Scene`, an `OrthographicCamera` (since we are rendering a 2D plane), and a `WebGLRenderer`. - **Geometry**: A simple `PlaneGeometry` that covers the entire camera's view will be created. This plane is the surface onto which our shader will be "painted." - **Material & Uniforms**: The core of the bridge between JS and GLSL. - A `ShaderMaterial` will be used, taking our custom fragment shader and a minimal, default vertex shader. - **Uniforms**: These variables are passed from JavaScript to the shader on every frame. We will define: - `u_time`: A `float` to drive all animations (fractal morphing, color pulsing). - `u_resolution`: A `vec2` holding the width and height of the canvas for aspect ratio correction. - `u_zoom`: A `float` to control the zoom level, modified by the mouse wheel. - `u_offset`: A `vec2` to control the panning of the view, modified by mouse dragging. - `u_mouse`: A `vec2` to pass the normalized mouse coordinates for interactive morphing. - **Interactivity & Animation**: - An `animate` loop using `requestAnimationFrame` will update the `u_time` uniform (if not paused) and render the scene. - **Event Listeners**: - `mousemove`: To update the `u_mouse` uniform and handle panning logic. - `mousedown`/`mouseup`: To toggle the panning state. - `wheel`: To adjust the `u_zoom` uniform and calculate the correct `u_offset` to make the zoom centered on the cursor. - `keydown`: To listen for the space bar to toggle a `paused` state. - `resize`: To update the renderer and `u_resolution` when the window changes size. #### 3. GLSL Fragment Shader Logic This is where the fractal is calculated and colored for every pixel. - **Uniforms**: Declare the uniforms received from JavaScript (`u_time`, `u_resolution`, etc.). - **Coordinate Mapping**: The shader starts by taking the pixel's screen coordinate (`gl_FragCoord.xy`) and transforming it into a point in the complex plane. This involves: 1. Normalizing the coordinates. 2. Correcting for aspect ratio using `u_resolution`. 3. Applying the zoom (`u_zoom`) and pan (`u_offset`) transformations. - **Julia Set Calculation**: 1. **Define `c`**: The complex constant `c` will be calculated based on `u_time` and `u_mouse.x`. A base circular path `(cos(time), sin(time))` will be modulated by the mouse's horizontal position, creating the interactive morphing effect. 2. **Iteration Loop**: For each pixel's coordinate `z`, we repeatedly apply the formula `z = z² + c`. We count the number of iterations until the magnitude of `z` exceeds a boundary (typically 2.0), or until a maximum number of iterations is reached. - **Coloring**: 1. The final color is determined by the number of iterations it took for the point to "escape". 2. To avoid "color banding," a smooth iteration count will be calculated using a logarithmic formula. 3. **Palette**: A function will map the smooth iteration count to the specified color gradient: deep indigo -> magenta -> fiery orange -> golden yellow. Chained `mix()` functions will be used for smooth blending. 4. **Pulsing Colors**: The `u_time` uniform will be used again to shift the phase of the color gradient, making the colors appear to breathe and evolve over time. 5. Points that do not escape (the set itself) will be colored a dark, deep indigo. - **Output**: The final calculated color is assigned to `gl_FragColor`. --- ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive GLSL Julia Set</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } canvas { display: block; } </style> </head> <body> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.164.1/build/three.module.js" } } </script> <script id="fragmentShader" type="x-shader/x-fragment"> uniform vec2 u_resolution; uniform float u_time; uniform float u_zoom; uniform vec2 u_offset; uniform vec2 u_mouse; const int MAX_ITER = 256; // Color Palette const vec3 C_INDIGO = vec3(0.18, 0.0, 0.51); // #2e0082 const vec3 C_MAGENTA = vec3(1.0, 0.0, 0.58); // #ff0094 const vec3 C_ORANGE = vec3(1.0, 0.35, 0.0); // #ff5900 const vec3 C_YELLOW = vec3(1.0, 0.84, 0.0); // #ffd600 // Smoothstep function for nicer transitions float smoothstep(float edge0, float edge1, float x) { float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); return t * t * (3.0 - 2.0 * t); } // Function to get color from our palette based on a value t (0.0 to 1.0) vec3 getPaletteColor(float t) { t = mod(t, 1.0); vec3 col; if (t < 0.33) { col = mix(C_INDIGO, C_MAGENTA, smoothstep(0.0, 0.33, t)); } else if (t < 0.66) { col = mix(C_MAGENTA, C_ORANGE, smoothstep(0.33, 0.66, t)); } else { col = mix(C_ORANGE, C_YELLOW, smoothstep(0.66, 1.0, t)); } return col; } void main() { // 1. Map pixel coordinates to the complex plane vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / u_resolution.y; vec2 z = uv / u_zoom + u_offset; // 2. Define the Julia constant 'c' with animation and mouse interaction float time = u_time * 0.1; float mouse_influence = (u_mouse.x - 0.5) * 0.4; vec2 c = vec2( 0.7885 * cos(time + mouse_influence * 2.0), 0.7885 * sin(time + mouse_influence * 2.0) ); // 3. Julia Set iteration int i; for(i = 0; i < MAX_ITER; i++) { // z = z*z + c z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; if(dot(z, z) > 4.0) { break; } } // 4. Coloring vec3 color; if (i == MAX_ITER) { color = C_INDIGO * 0.5; // Inside the set } else { // Smooth iteration count for continuous coloring float smooth_i = float(i) - log2(log2(dot(z, z))) + 4.0; // Map smoothed iteration count to a color value float color_value = smooth_i / (64.0); // Animate the color phase over time float color_phase = u_time * 0.05; // Get final color from palette color = getPaletteColor(color_value + color_phase); } gl_FragColor = vec4(color, 1.0); } </script> <script type="module"> import * as THREE from 'three'; // Basic setup const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); const renderer = new THREE.WebGLRenderer(); document.body.appendChild(renderer.domElement); // State const mouse = new THREE.Vector2(); let isDragging = false; let paused = false; const prevMouse = new THREE.Vector2(); const uniforms = { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2() }, u_mouse: { value: new THREE.Vector2() }, u_zoom: { value: 0.5 }, u_offset: { value: new THREE.Vector2(0.0, 0.0) } }; // Geometry and Material const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: uniforms, fragmentShader: document.getElementById('fragmentShader').textContent }); const plane = new THREE.Mesh(geometry, material); scene.add(plane); camera.position.z = 1; // --- Event Listeners --- function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.x = renderer.domElement.width; uniforms.u_resolution.value.y = renderer.domElement.height; } function onMouseMove(event) { mouse.x = event.clientX / window.innerWidth; mouse.y = 1.0 - event.clientY / window.innerHeight; // Invert Y uniforms.u_mouse.value.copy(mouse); if (isDragging) { const deltaX = event.clientX - prevMouse.x; const deltaY = event.clientY - prevMouse.y; uniforms.u_offset.value.x -= deltaX / (window.innerHeight * uniforms.u_zoom.value); uniforms.u_offset.value.y += deltaY / (window.innerHeight * uniforms.u_zoom.value); prevMouse.set(event.clientX, event.clientY); } } function onMouseDown(event) { if (event.button === 0) { // Left click isDragging = true; prevMouse.set(event.clientX, event.clientY); } } function onMouseUp(event) { isDragging = false; } function onWheel(event) { event.preventDefault(); const zoomFactor = 1.1; const zoomAmount = event.deltaY < 0 ? zoomFactor : 1 / zoomFactor; // Get mouse position in shader coordinate space before zoom const mouseX = mouse.x * 2.0 - 1.0; const mouseY = mouse.y * 2.0 - 1.0; const aspectRatio = uniforms.u_resolution.value.x / uniforms.u_resolution.value.y; const pointBeforeZoom = new THREE.Vector2( (mouseX * aspectRatio) / uniforms.u_zoom.value + uniforms.u_offset.value.x, mouseY / uniforms.u_zoom.value + uniforms.u_offset.value.y ); // Apply zoom uniforms.u_zoom.value *= zoomAmount; // Get mouse position in shader coordinate space after zoom const pointAfterZoom = new THREE.Vector2( (mouseX * aspectRatio) / uniforms.u_zoom.value + uniforms.u_offset.value.x, mouseY / uniforms.u_zoom.value + uniforms.u_offset.value.y ); // Adjust offset to keep the point under the cursor uniforms.u_offset.value.x += pointBeforeZoom.x - pointAfterZoom.x; uniforms.u_offset.value.y += pointBeforeZoom.y - pointAfterZoom.y; } function onKeyDown(event) { if (event.code === 'Space') { event.preventDefault(); paused = !paused; } } // Attach listeners window.addEventListener('resize', onWindowResize, false); document.addEventListener('mousemove', onMouseMove, false); document.addEventListener('mousedown', onMouseDown, false); document.addEventListener('mouseup', onMouseUp, false); document.addEventListener('wheel', onWheel, { passive: false }); document.addEventListener('keydown', onKeyDown, false); // --- Animation Loop --- const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); if (!paused) { uniforms.u_time.value += clock.getDelta(); } renderer.render(scene, camera); } // Initial setup and start onWindowResize(); onMouseMove({ clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 }); // Center mouse initially animate(); </script> </body> </html> ```
**Action Plan for Interactive Animated Julia Set Fractal Art** --- ## 1. HTML Structure - **Minimal Body**: Only a `<body>` containing a canvas created by Three.js. - **Resource Loading**: Load Three.js via CDN. - **Embedded Script**: JavaScript initializing Three.js, handling events, and animation loop. All code contained in the HTML, no external dependencies. --- ## 2. Three.js Setup - **Renderer**: WebGLRenderer with antialias. - **Camera**: OrthographicCamera to keep coordinates linear and suitable for 2D mapping. - **Scene**: One PlaneGeometry mesh covering the viewport with a ShaderMaterial using our custom GLSL. - **Resizing**: Responsive canvas scaling. --- ## 3. User Interaction ### A. Zoom - Mouse wheel event. - Fractal view window defined by center and zoom level; zoom is centered on the mouse position in fractal space. ### B. Pan - Mouse drag: on mousedown, record origin; on mousemove, update the view’s center accordingly, recalculating based on drag direction and current zoom. ### C. Morphing Control - Mouse X position influences the path’s parameters—e.g., the ellipse’s aspect ratio or radius/phase—of the animated constant c over its circular path. ### D. Pause - Space bar toggles play/pause of both: - The animation (time variable) - The color palette shifting. --- ## 4. GLSL Fragment Shader ### A. Uniforms - `u_resolution` – vec2 (viewport size) - `u_center` – vec2 (center of fractal view, in complex plane) - `u_zoom` – float (zoom factor) - `u_time` – float (animation phase/time) - `u_c` – vec2 (Julia constant, animated in JS) - `u_paletteShift` – float (controls palette phase/pulsing) ### B. Main Logic 1. **Map each fragment** to a complex coordinate in fractal space, using uniforms for center, zoom, aspect ratio. 2. **Iterate** z = z^2 + c, tracking number of steps before |z|>2 or maxIterations. 3. **Coloring**: - Smooth iteration count (orbit trap or normalized iteration count). - Pass smooth count and `u_paletteShift` to a palette function. - Palette function interpolates through four key colors: indigo, magenta, orange, yellow, blending and pulsing with time. --- ## 5. Animation & Uniform Updates - `u_time` increases with real time unless paused. - `u_c` is animated in a circular path in the complex plane—parameters (radius, shape, or phase speed) modified by mouse X. - `u_paletteShift` is a slow function of time for smooth color cycling, also stopped/started by pause. --- ## 6. Complete Code Structure - **<!DOCTYPE html>** - `<head>` loads Three.js, sets `<meta>` for viewport. - `<body>` empty. - `<script>` block: 1. Setup Three.js scene. 2. Attach event listeners for interactivity (zoom, pan, pause, morphing). 3. Animation/render loop: update uniforms & render. 4. Shader code as GLSL strings in JS. 5. Responsive canvas and window resizes. 6. Clean up on unload (optional). --- ## 7. Key Algorithm/Pseudocode ```javascript // Mouse wheel -> adjust zoom, recenter on cursor in fractal space // Mouse drag -> change center // Mouse move -> affect morph parameters for animated c // Animation loop: // - If !paused: update time and palette shift // - Calculate c as function of (time, mouseX) // - Pass uniforms to shader and render // Shader fragment: // For each pixel: // - Map to complex plane // - Iterate z = z^2 + c up to maxIterations // - Smoothly calculate iteration count // - Use palette function (with current palette shift) for final color ``` --- **Summary**: The code creates a Three.js orthographic scene with one shader-powered mesh. All fractal computation and color are in the GLSL fragment shader, directed by uniforms for animation and interactivity. Mouse and keyboard events allow zooming, panning, morphing, and pausing. The Julia parameter c is animated in a path modulated by mouseX; color palette smoothly transitions as specified. The whole experience is contained in a single HTML file, ready to run anywhere. --- **Ready to proceed with the full code according to this plan.**
Here’s the high-level plan before diving into code: 1. HTML Structure - A single HTML file with: • A `<head>` including the Three.js CDN `<script>` tag. • A `<body>` that is effectively empty—everything will be drawn into a full-window WebGL canvas created by Three.js. • A single `<script>` block at the end that contains all our JS and our GLSL shader source as strings. 2. JavaScript Setup (Three.js) a. Scene, Camera, Renderer - Create a Three.js `Scene`. - Use an orthographic or perspective `Camera` positioned so that a single fullscreen quad fills the view. - Create a `WebGLRenderer`, set its size to `window.innerWidth/innerHeight`, append `renderer.domElement` to document body. - Listen for `window.resize` to update renderer size and a `u_resolution` uniform. b. Geometry & Shader Material - Build a `PlaneBufferGeometry(2, 2)` that covers the NDC space. - Write a `ShaderMaterial` with: • A trivial vertex shader that passes `position` → `v_uv` (mapped from –1..+1 to 0..1). • A fragment shader implementing the Julia-set logic. • Uniforms: – `u_time` (float): accumulated time, drives animation. – `u_colorTime` (float): time for color pulsing (can be same as `u_time` or a slowed version). – `u_resolution` (vec2): canvas pixel dimensions. – `u_zoom` (float): current zoom factor. – `u_offset` (vec2): pan offset in complex plane. – `u_c` (vec2): current Julia constant c = (cx, cy). – `u_mouseX` (float): normalized [0..1] mouse X, to modulate c’s animation path. – `u_maxIter` (int): maximum iteration count. c. Input Handling & Interaction - Mouse wheel → zoom in/out around cursor: • On `wheel` event, compute new `u_zoom` = oldZoom × exp(Δ * zoomSpeed). • Compute world-space cursor coordinate before/after zoom to update `u_offset` so the point under cursor stays fixed. - Mouse drag → pan: • On `mousedown`, record start position; on `mousemove` (with button down), compute delta in NDC, convert to complex-plane delta = deltaNDC * (2/zoom), and subtract that from `u_offset`. - Mouse move → update `u_mouseX` = event.clientX / window.innerWidth. - Keyboard space bar → toggle a `paused` boolean. When `paused`, stop advancing `u_time` and `u_colorTime`. d. Animation Loop - Use `requestAnimationFrame(animate)`. - If not `paused`, increment two JS time accumulators (`timeAccum`, `colorAccum`) by deltaTime. - Compute the animated Julia constant c in JS each frame: • Let base radius R = 0.7885 (for interesting shapes). • Let base speed ω = 0.2. • Let phase offset φ = u_mouseX × 2π (so moving the mouse shifts the starting phase). • c = R * vec2(cos(ω * timeAccum + φ), sin(ω * timeAccum + φ)). • Pass c into `u_c`. - Update all uniforms: `u_time = timeAccum`, `u_colorTime = colorAccum`, `u_zoom`, `u_offset`, `u_mouseX`, etc. - Render the scene: `renderer.render(scene, camera)`. 3. GLSL Fragment Shader Logic a. Map fragment coordinates → complex plane - Compute `vec2 uv = (gl_FragCoord.xy / u_resolution) * 2.0 - 1.0;` - Adjust aspect ratio: `uv.x *= u_resolution.x / u_resolution.y;` - Compute initial z: `z = uv / u_zoom + u_offset;` b. Julia iteration - Loop for `i` from 0 to `u_maxIter`: • `z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + u_c;` • If `dot(z, z) > 4.0`, break. - For smooth coloring, compute `float mu = i + 1.0 - log2(log2(length(z)));` c. Color mapping - Normalize `t = mu / float(u_maxIter)`. - Apply a time-pulsing shift: `t = fract(t + 0.1 * sin(u_colorTime * 0.5));` - Build a 4-color gradient: • Define fixed color stops in vec3: deep indigo, magenta, fiery orange, golden yellow. • If `t < 1/3`, mix(indigo, magenta, t * 3); else if `< 2/3`, mix(magenta, orange, (t-1/3)*3); else mix(orange, yellow, (t-2/3)*3). d. Output - `gl_FragColor = vec4(color, 1.0);` That plan gives us a complete roadmap: a single HTML file, Three.js scene with a fullscreen shader quad, JS uniforms driving a GLSL fragment shader that does the iteration, smooth coloring, animated c, zoom/pan interactivity, and pause control.
# Interactive Julia Set Fractal Art I'll create an interactive fractal visualization using Three.js and GLSL shaders. The implementation will feature a morphing Julia set with color transitions, zoom/pan controls, and animation controls. ## Action Plan 1. **HTML Structure**: Create a full-screen canvas with Three.js 2. **Three.js Setup**: - Initialize scene, camera, and renderer - Create a full-screen quad for fragment shader rendering - Set up uniforms for shader parameters 3. **GLSL Fragment Shader**: - Implement Julia set fractal algorithm - Create smooth color gradient transitions - Handle zooming and panning in the complex plane 4. **Interactivity**: - Mouse wheel for zooming (centered at cursor) - Mouse drag for panning - Mouse X position to control fractal morphing - Space bar to pause/resume animations 5. **Animation**: - Animate the Julia constant c in a circular path - Pulse color gradients over time ## Implementation ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; background: #000; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } canvas { display: block; } .info { position: absolute; bottom: 20px; left: 20px; color: rgba(255, 255, 255, 0.7); font-size: 14px; background: rgba(0, 0, 0, 0.5); padding: 10px 15px; border-radius: 10px; pointer-events: none; } .title { position: absolute; top: 20px; left: 0; right: 0; text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 24px; text-shadow: 0 0 10px rgba(255, 100, 255, 0.7); pointer-events: none; } </style> </head> <body> <div class="title">Interactive Julia Set Fractal</div> <div class="info">Scroll: Zoom | Drag: Pan | Move Mouse: Change Shape | Space: Pause</div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Main variables let scene, camera, renderer; let material, mesh; let uniforms; let paused = false; let time = 0; let mouseX = 0.5, mouseY = 0.5; let zoom = 1.0; let center = new THREE.Vector2(0, 0); let startDragPos = new THREE.Vector2(); let startCenter = new THREE.Vector2(); let isDragging = false; // Initialize Three.js function init() { // Create scene scene = new THREE.Scene(); // Create camera (orthographic for 2D rendering) const aspect = window.innerWidth / window.innerHeight; camera = new THREE.OrthographicCamera( -aspect, aspect, 1, -1, 0.1, 10 ); camera.position.z = 1; // Create renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); // Create full-screen quad const geometry = new THREE.PlaneGeometry(2, 2); // Define shader uniforms uniforms = { u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_time: { value: 0 }, u_mouse: { value: new THREE.Vector2(mouseX, mouseY) }, u_zoom: { value: zoom }, u_center: { value: center }, u_c: { value: new THREE.Vector2() } }; // Create shader material material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` precision highp float; uniform vec2 u_resolution; uniform float u_time; uniform vec2 u_mouse; uniform float u_zoom; uniform vec2 u_center; uniform vec2 u_c; const int MAX_ITER = 150; // Smooth color palette function vec3 palette(float t) { vec3 col1 = vec3(0.2, 0.0, 0.4); // Deep indigo vec3 col2 = vec3(0.8, 0.0, 0.6); // Magenta vec3 col3 = vec3(1.0, 0.4, 0.1); // Fiery orange vec3 col4 = vec3(1.0, 0.9, 0.1); // Golden yellow // Add pulsing effect with time float pulse = sin(u_time * 0.3) * 0.05; t = mod(t + pulse, 1.0); // Create smooth gradient transitions if (t < 0.33) { return mix(col1, col2, t * 3.0); } else if (t < 0.66) { return mix(col2, col3, (t - 0.33) * 3.0); } else { return mix(col3, col4, (t - 0.66) * 3.0); } } void main() { // Normalize pixel coordinates vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); // Apply zoom and pan uv *= u_zoom; uv += u_center; // Julia set parameters vec2 c = u_c; // Initialize complex number z vec2 z = uv; // Iteration count int iter = 0; // Julia set iteration for (int i = 0; i < MAX_ITER; i++) { // Complex multiplication: z = z^2 + c float x = (z.x * z.x - z.y * z.y) + c.x; float y = (2.0 * z.x * z.y) + c.y; z = vec2(x, y); // Break if escaped if (dot(z, z) > 4.0) { iter = i; break; } } // Normalize iteration count float t = float(iter) / float(MAX_ITER); // Apply smooth coloring vec3 color = palette(t); // Add subtle vignette effect vec2 uvNorm = gl_FragCoord.xy / u_resolution; float vignette = 1.0 - smoothstep(0.7, 1.0, length(uvNorm - 0.5)); color *= vignette; // Output final color gl_FragColor = vec4(color, 1.0); } ` }); // Create mesh and add to scene mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Event listeners window.addEventListener('resize', onWindowResize); window.addEventListener('mousemove', onMouseMove); window.addEventListener('mousedown', onMouseDown); window.addEventListener('mouseup', onMouseUp); window.addEventListener('wheel', onMouseWheel); window.addEventListener('keydown', onKeyDown); // Start animation loop animate(); } // Handle window resize function onWindowResize() { const width = window.innerWidth; const height = window.innerHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); uniforms.u_resolution.value.set(width, height); } // Mouse movement handler function onMouseMove(event) { mouseX = event.clientX / window.innerWidth; mouseY = 1.0 - event.clientY / window.innerHeight; // Invert Y for GL space uniforms.u_mouse.value.set(mouseX, mouseY); if (isDragging) { const currentX = event.clientX / window.innerWidth; const currentY = event.clientY / window.innerHeight; const dx = (currentX - startDragPos.x) * 2.0 / zoom; const dy = (startDragPos.y - currentY) * 2.0 / zoom; center.x = startCenter.x + dx; center.y = startCenter.y + dy; uniforms.u_center.value.copy(center); } } // Mouse down handler (start panning) function onMouseDown(event) { isDragging = true; startDragPos.set(event.clientX / window.innerWidth, event.clientY / window.innerHeight); startCenter.copy(center); } // Mouse up handler (stop panning) function onMouseUp() { isDragging = false; } // Mouse wheel handler (zoom) function onMouseWheel(event) { event.preventDefault(); const zoomFactor = 0.1; const delta = event.deltaY > 0 ? -zoomFactor : zoomFactor; const newZoom = zoom * (1.0 + delta); // Limit zoom range zoom = Math.max(0.1, Math.min(newZoom, 100.0)); uniforms.u_zoom.value = zoom; // Adjust center for zooming toward mouse position const mouseX = event.clientX / window.innerWidth; const mouseY = event.clientY / window.innerHeight; const worldX = (mouseX - 0.5) * 2.0; const worldY = (0.5 - mouseY) * 2.0; center.x += worldX * (1.0/zoom - 1.0/newZoom); center.y += worldY * (1.0/zoom - 1.0/newZoom); uniforms.u_center.value.copy(center); } // Keyboard input handler function onKeyDown(event) { if (event.code === 'Space') { paused = !paused; } } // Animation loop function animate() { requestAnimationFrame(animate); // Update time if not paused if (!paused) { time += 0.016; // ~60fps uniforms.u_time.value = time; // Update Julia constant c with circular motion const phase = mouseX * 3.0; // Mouse X controls phase const radius = 0.7885; const speed = 0.2; uniforms.u_c.value.set( radius * cos(time * speed + phase), radius * sin(time * speed + phase) ); } renderer.render(scene, camera); } // Initialize when page loads window.onload = init; </script> </body> </html> ``` ## Key Features 1. **Julia Set Fractal**: Rendered entirely in a GLSL fragment shader for maximum performance 2. **Dynamic Morphing**: The Julia constant c follows a circular path that evolves over time 3. **Interactive Controls**: - Mouse wheel: Zoom in/out centered on cursor position - Mouse drag: Pan around the fractal - Mouse movement: Control the fractal's morphing pattern - Space bar: Toggle animation pause/resume 4. **Color Transitions**: Smooth gradient from deep indigo to golden yellow with: - Color pulsing effect synchronized with time - Vignette effect for visual polish 5. **Responsive Design**: Automatically adjusts to window resizing The implementation creates a mesmerizing, interactive fractal experience that responds to user input while maintaining high performance through GPU-accelerated rendering.
<html> <head> <meta charset="utf-8"> <title>Interactive Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } canvas { display: block; } .info { position: absolute; bottom: 10px; left: 10px; color: white; font-family: monospace; background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; user-select: none; pointer-events: none; } </style> </head> <body> <div class="info"> Mouse wheel: Zoom | Mouse drag: Pan | Left/Right: Influence shape | Space: Pause/Play </div> <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> <script type="x-shader/x-fragment" id="fragmentShader"> uniform vec2 resolution; uniform float time; uniform vec2 c; uniform vec2 pan; uniform float zoom; const int MAX_ITERATIONS = 300; const float ESCAPE_RADIUS = 4.0; // Smooth color palette based on cosine functions vec3 palette(float t) { vec3 a = vec3(0.5, 0.5, 0.5); vec3 b = vec3(0.5, 0.5, 0.5); vec3 c = vec3(1.0, 1.0, 1.0); vec3 d = vec3(0.2, 0.1, 0.4); // Control the phase for different colors // Indigo to magenta to orange to yellow return a + b * cos(6.28318 * (c * t + d + sin(time * 0.1) * 0.1)); } void main() { vec2 uv = (gl_FragCoord.xy - 0.5 * resolution) / min(resolution.x, resolution.y); // Apply zoom and pan uv = uv / zoom + pan; vec2 z = uv; int iter; float smooth_iter = 0.0; for (iter = 0; iter < MAX_ITERATIONS; iter++) { // z = z^2 + c (complex multiplication and addition) 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) > ESCAPE_RADIUS) { // Smooth coloring formula smooth_iter = float(iter) - log2(log2(dot(z, z))) + 4.0; break; } } if (iter == MAX_ITERATIONS) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Points in the set are black } else { // Normalize the iteration count and apply color palette float t = smooth_iter / float(MAX_ITERATIONS); vec3 color = palette(t); gl_FragColor = vec4(color, 1.0); } } </script> <script> let scene, camera, renderer, material; let clock = new THREE.Clock(); let paused = false; let mouseX = 0; let mouseDown = false; let lastMousePos = { x: 0, y: 0 }; let pan = { x: 0, y: 0 }; let zoom = 1.0; init(); animate(); function init() { // Create scene scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Create a full-screen quad const geometry = new THREE.PlaneBufferGeometry(2, 2); // Create shader material material = new THREE.ShaderMaterial({ uniforms: { resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, time: { value: 0.0 }, c: { value: new THREE.Vector2(0.285, 0.01) }, // Initial Julia set constant pan: { value: new THREE.Vector2(0.0, 0.0) }, zoom: { value: 1.0 } }, fragmentShader: document.getElementById('fragmentShader').textContent }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Event listeners window.addEventListener('resize', onWindowResize); window.addEventListener('wheel', onWheel); window.addEventListener('mousedown', onMouseDown); window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); window.addEventListener('keydown', onKeyDown); } function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight); } function onWheel(event) { event.preventDefault(); // Calculate zoom factor const zoomFactor = 1.1; if (event.deltaY > 0) { zoom /= zoomFactor; } else { zoom *= zoomFactor; } // Limit zoom zoom = Math.max(0.01, Math.min(zoom, 1000)); // Get mouse position in normalized device coordinates const rect = renderer.domElement.getBoundingClientRect(); const mouseXNDC = ((event.clientX - rect.left) / rect.width) * 2 - 1; const mouseYNDC = -((event.clientY - rect.top) / rect.height) * 2 + 1; // Calculate how much to adjust pan based on mouse position and zoom change const aspect = window.innerWidth / window.innerHeight; let zoomPanX = mouseXNDC * (1 / zoom - 1 / (zoom * (event.deltaY > 0 ? zoomFactor : 1/zoomFactor))) * (aspect > 1 ? aspect : 1); let zoomPanY = mouseYNDC * (1 / zoom - 1 / (zoom * (event.deltaY > 0 ? zoomFactor : 1/zoomFactor))) * (aspect < 1 ? 1/aspect : 1); // Update pan to zoom toward mouse position pan.x += zoomPanX; pan.y += zoomPanY; } function onMouseDown(event) { mouseDown = true; lastMousePos.x = event.clientX; lastMousePos.y = event.clientY; } function onMouseMove(event) { // Update mouseX for influencing the Julia Set constant mouseX = (event.clientX / window.innerWidth) * 2 - 1; if (mouseDown) { // Calculate mouse movement in screen space const deltaX = event.clientX - lastMousePos.x; const deltaY = event.clientY - lastMousePos.y; // Convert to world space (considering zoom) const aspect = window.innerWidth / window.innerHeight; pan.x -= deltaX / (0.5 * window.innerWidth) * (aspect > 1 ? aspect : 1) / zoom; pan.y += deltaY / (0.5 * window.innerHeight) * (aspect < 1 ? 1/aspect : 1) / zoom; lastMousePos.x = event.clientX; lastMousePos.y = event.clientY; } } function onMouseUp() { mouseDown = false; } function onKeyDown(event) { if (event.code === 'Space') { paused = !paused; if (!paused) { clock.start(); // Restart the clock to avoid jumps } } } function animate() { requestAnimationFrame(animate); if (!paused) { const elapsedTime = clock.getElapsedTime(); // Update Julia Set constant c for animation // Create a circular path influenced by mouseX const baseSpeed = 0.2; const radius = 0.1 + mouseX * 0.1; const offset = 0.3 + mouseX * 0.2; material.uniforms.c.value.x = offset + radius * Math.cos(elapsedTime * baseSpeed); material.uniforms.c.value.y = radius * Math.sin(elapsedTime * baseSpeed); material.uniforms.time.value = elapsedTime; } // Update pan and zoom uniforms material.uniforms.pan.value.copy(pan); material.uniforms.zoom.value = zoom; renderer.render(scene, camera); } </script> </body> </html>
Below is my detailed plan for creating the interactive, animated Julia Set fractal art piece as specified. After the plan, I'll provide the complete, self-contained HTML file that includes all necessary code. ### Action Plan To achieve the mission, I'll structure the project as a single HTML file that incorporates Three.js via a CDN for rendering the fractal. The core fractal rendering will be handled in a GLSL fragment shader, ensuring that all visual and interactive elements are managed efficiently. 1. **HTML Structure:** - The HTML file will start with a basic doctype and structure: `<html>`, `<head>`, and `<body>`. - In the `<head>`, I'll include a `<script>` tag to load Three.js from a CDN (e.g., from unpkg.com). This ensures no external dependencies beyond the CDN are needed. - In the `<body>`, I'll add a `<canvas>` element for Three.js to render the scene. I'll also include another `<script>` tag for the custom JavaScript code, which sets up the scene, handles interactions, and manages the animation loop. - The entire file will be self-contained, with all scripts inline. 2. **JavaScript Setup with Three.js:** - **Scene Initialization:** I'll create a Three.js scene, an orthographic camera (for 2D fractal rendering), and a WebGL renderer that targets the canvas. The camera will be set up to cover the full screen initially. - **Geometry and Material:** I'll use a simple PlaneGeometry (e.g., 2x2 units) to create a full-screen plane. This plane will be assigned a custom ShaderMaterial that includes the vertex and fragment shaders. - **Uniforms for the Shader:** The ShaderMaterial will define several uniforms to pass data from JavaScript to the GLSL shader: - `u_time`: A float that increments over time to animate the Julia Set constant `c` and shift colors. - `u_resolution`: A vec2 for the canvas resolution, used for mapping pixel coordinates. - `u_mouse`: A vec2 for the current mouse position, which influences the animation path of `c`. - `u_zoom`: A float to control the zoom level, adjusted based on mouse wheel events. - `u_offset`: A vec2 to handle panning, updated during mouse drag events. - `u_paused`: A float (0 or 1) to toggle animations based on the space bar. - **Animation Loop:** I'll use `requestAnimationFrame` to create a loop that updates the uniforms (e.g., increment `u_time` if not paused), handles interactions, and renders the scene. - **Interactivity Handling:** - **Zoom:** Listen for the 'wheel' event on the canvas. Calculate the new zoom level centered on the mouse position and update `u_zoom` and `u_offset` accordingly. - **Pan:** Listen for 'mousedown', 'mousemove', and 'mouseup' events. When dragging, track the mouse movement and update `u_offset` to shift the view. - **Morphing Control:** In the animation loop, use the mouse's X position to modify the animation of the Julia Set constant `c`. For example, add the normalized mouse X value to the angle of a circular path. - **Pause:** Listen for 'keydown' events and toggle a pause state when the space bar is pressed. This will stop updating `u_time`, effectively pausing all animations. - **Error Handling:** Basic checks will ensure the canvas and renderer are properly initialized. 3. **Logic for the GLSL Fragment Shader:** - **Uniforms in the Shader:** The fragment shader will receive the uniforms listed above. These will be used to compute the fractal and colors. - **Main Rendering Loop:** - **Coordinate Mapping:** In the `main()` function, map the fragment's UV coordinates to the complex plane, scaled by `u_zoom` and offset by `u_offset`. This allows for zooming and panning. - **Julia Set Calculation:** For each fragment, iterate the Julia Set formula: Start with `z` as the mapped coordinate, and iteratively compute `z = z * z + c`, where `c` is derived from `u_time` and influenced by `u_mouse`. Specifically: - Animate `c` in a circular path: e.g., `c.x = radius * cos(u_time + u_mouse.x * influenceFactor)`, `c.y = radius * sin(u_time + u_mouse.x * influenceFactor)`. - Use a fixed maximum number of iterations (e.g., 100) and count how many iterations it takes for `|z| > 2`. - **Coloring Logic:** Based on the iteration count, assign a color from a smooth gradient ("deep indigo → magenta → fiery orange → golden yellow"). Use a function like `mix` or `smoothstep` to interpolate colors. Additionally, shift the gradient over time by offsetting the color calculation with `u_time` (e.g., add `u_time` to the hue in HSV space). Points that don't escape within the max iterations can be colored black or a deep color for the fractal boundary. - **Output:** Set the fragment color based on the iteration count and time-based pulsing. This plan ensures the output is visually stunning, interactive, and performant, while adhering to the specifications. Now, here's the complete HTML file with all the code. ``` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Interactive Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; } /* Full-screen canvas */ canvas { display: block; } </style> <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script> <script> // Custom JavaScript code starts here let scene, camera, renderer, mesh; let uniforms; let paused = false; let mouse = { x: 0, y: 0 }; let previousMouse = { x: 0, y: 0 }; let isDragging = false; let time = 0; function init() { scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1; renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); uniforms = { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_mouse: { value: new THREE.Vector2(0.0, 0.0) }, u_zoom: { value: 1.0 }, u_offset: { value: new THREE.Vector2(0.0, 0.0) }, u_paused: { value: 0.0 } }; const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float u_time; uniform vec2 u_resolution; uniform vec2 u_mouse; uniform float u_zoom; uniform vec2 u_offset; uniform float u_paused; varying vec2 vUv; vec3 palette(float t) { // Gradient: deep indigo -> magenta -> fiery orange -> golden yellow vec3 a = vec3(0.29, 0.0, 0.51); // Deep indigo vec3 b = vec3(1.0, 0.0, 1.0); // Magenta vec3 c = vec3(1.0, 0.5, 0.0); // Fiery orange vec3 d = vec3(1.0, 0.84, 0.0); // Golden yellow return mix(mix(a, b, smoothstep(0.0, 0.33, t)), mix(c, d, smoothstep(0.33, 1.0, t)), smoothstep(0.0, 1.0, fract(t + u_time * 0.1))); // Pulse with time } void main() { vec2 res = u_resolution; vec2 uv = (vUv * 2.0 - 1.0) * u_zoom + u_offset; // Apply zoom and offset uv.x *= res.x / res.y; // Aspect ratio correction float aspect = res.x / res.y; vec2 c = vec2(0.7885 * cos(u_time + u_mouse.x * 2.0), 0.7885 * sin(u_time + u_mouse.x * 2.0)); // Circular path, influenced by mouse X vec2 z = uv; const int maxIter = 100; int iter; for(iter = 0; iter < maxIter; iter++) { z = vec2(z.x * z.x - z.y * z.y + c.x, 2.0 * z.x * z.y + c.y); if(dot(z, z) > 4.0) break; } float colorVal = float(iter) / float(maxIter); vec3 color = palette(colorVal); // Apply palette with time-based shift if(iter == maxIter) color = vec3(0.0); // Black for inside the set gl_FragColor = vec4(color, 1.0); } ` }); mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Event listeners renderer.domElement.addEventListener('mousemove', onMouseMove); renderer.domElement.addEventListener('mousedown', onMouseDown); renderer.domElement.addEventListener('mouseup', onMouseUp); renderer.domElement.addEventListener('wheel', onWheel); window.addEventListener('keydown', onKeyDown); window.addEventListener('resize', onWindowResize); } function animate() { requestAnimationFrame(animate); if (!paused) { time += 0.01; // Slow animation uniforms.u_time.value = time; } uniforms.u_paused.value = paused ? 1.0 : 0.0; renderer.render(scene, camera); } function onMouseMove(event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; // Normalized mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; uniforms.u_mouse.value.set(mouse.x, mouse.y); if (isDragging) { const dx = event.clientX - previousMouse.x; const dy = event.clientY - previousMouse.y; uniforms.u_offset.value.x += dx * 0.005 * (1.0 / uniforms.u_zoom.value); // Adjust for zoom uniforms.u_offset.value.y -= dy * 0.005 * (1.0 / uniforms.u_zoom.value); previousMouse = { x: event.clientX, y: event.clientY }; } } function onMouseDown(event) { isDragging = true; previousMouse = { x: event.clientX, y: event.clientY }; } function onMouseUp() { isDragging = false; } function onWheel(event) { event.preventDefault(); const zoomFactor = 1.1; const mouseBefore = vec2ToWorld(event.clientX, event.clientY); uniforms.u_zoom.value *= event.deltaY > 0 ? zoomFactor : 1 / zoomFactor; const mouseAfter = vec2ToWorld(event.clientX, event.clientY); uniforms.u_offset.value += mouseBefore - mouseAfter; // Center zoom on cursor } function vec2ToWorld(x, y) { const ndcX = (x / window.innerWidth) * 2 - 1; const ndcY = -(y / window.innerHeight) * 2 + 1; return new THREE.Vector2( ndcX * uniforms.u_zoom.value + uniforms.u_offset.value.x, ndcY * uniforms.u_zoom.value + uniforms.u_offset.value.y ); } function onKeyDown(event) { if (event.code === 'Space') { paused = !paused; } } function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.set(window.innerWidth, window.innerHeight); camera.left = -1 * window.innerWidth / window.innerHeight; camera.right = 1 * window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); } init(); animate(); </script> </head> <body> </body> </html> ```