
Animated Fractal with Three.js
This microeval asks the LLM to produce a single HTML code block with optional CSS JavaScript and GLSL that renders a full screen Julia set shader animation with smooth color transitions from golden yellow through orange magenta purple to deep indigo, continuously morphs via a rotating complex constant, supports click and drag panning mouse wheel or pinch zoom and a space bar toggle for play and pause, and relies solely on Three.js loaded from a CDN.
Prompt
Create a Three.js animation that renders a morphing Julia-set fractal shader on full-screen. Compute many iterations in the fragment shader and apply smooth colors that transition from golden yellow through orange, magenta, and purple to deep indigo. Rotate the complex constant at a steady, moderate pace. Provide built-in click-and-drag panning and mouse-wheel (or pinch) zooming, and bind the space bar to toggle play and pause. Build it exclusively with Three.js—no other libraries or assets.
A system prompt was added to support web rendering
```html <!DOCTYPE html> <html lang="en"> <head> <title>Morphing Julia Set</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #000; } canvas { display: block; } .info { position: absolute; bottom: 10px; width: 100%; text-align: center; color: #fff; font-family: monospace; z-index: 100; pointer-events: none; /* Let clicks/drags pass through */ } </style> </head> <body> <div id="info" class="info"> [ Drag to Pan ] [ Scroll to Zoom ] [ Space to Pause/Play ] </div> <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'; // --- GLOBAL VARIABLES --- let scene, camera, renderer, mesh; let uniforms; let isPlaying = true; let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; const clock = new THREE.Clock(); // --- SHADERS --- const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragmentShader = ` // Varyings passed from the vertex shader varying vec2 vUv; // Uniforms passed from JavaScript uniform vec2 u_resolution; uniform float u_time; uniform float u_zoom; uniform vec2 u_pan; uniform vec2 u_c; // The complex constant for the Julia set // --- CONSTANTS --- const int MAX_ITER = 256; // --- COLOR PALETTE DEFINITIONS --- // Golden Yellow -> Orange -> Magenta -> Purple -> Deep Indigo vec3 color1 = vec3(1.0, 0.843, 0.0); // Golden Yellow vec3 color2 = vec3(1.0, 0.5, 0.0); // Orange vec3 color3 = vec3(1.0, 0.0, 1.0); // Magenta vec3 color4 = vec3(0.53, 0.0, 0.93); // Purple vec3 color5 = vec3(0.29, 0.0, 0.51); // Deep Indigo vec3 color_inside = vec3(0.0, 0.0, 0.05); // Color for points inside the set // Function to multiply two complex numbers (represented as vec2) vec2 complex_mult(vec2 a, vec2 b) { return vec2(a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x); } void main() { // Map screen coordinates (0-1) to complex plane coordinates // Adjust for aspect ratio, zoom, and pan vec2 aspect_ratio = vec2(u_resolution.x / u_resolution.y, 1.0); vec2 z = (vUv * 2.0 - 1.0) * aspect_ratio / u_zoom + u_pan; vec2 c = u_c; // Use the animated constant from JS int i; for (i = 0; i < MAX_ITER; i++) { z = complex_mult(z, z) + c; // Bailout condition if (dot(z, z) > 16.0) { break; } } // Color calculation vec3 color; if (i == MAX_ITER) { // This point is inside the set color = color_inside; } else { // This point is outside the set. Use a smooth coloring algorithm. // This uses the final magnitude of z to create a fractional iteration count. float smooth_i = float(i) - log2(log2(dot(z,z))) + 4.0; // Normalize the smooth iteration count to create a repeating gradient float t = smooth_i / (float(MAX_ITER)*0.2); t = fract(t); // Use the fractional part for seamless looping // Create the multi-stop gradient using mix and smoothstep vec3 col = mix(color1, color2, smoothstep(0.0, 0.25, t)); col = mix(col, color3, smoothstep(0.25, 0.5, t)); col = mix(col, color4, smoothstep(0.5, 0.75, t)); col = mix(col, color5, smoothstep(0.75, 1.0, t)); color = col; } gl_FragColor = vec4(color, 1.0); } `; // --- INITIALIZATION --- function init() { // Scene scene = new THREE.Scene(); // Camera // An orthographic camera is best for 2D shaders camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1; // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); // Shader Uniforms uniforms = { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_c: { value: new THREE.Vector2(-0.8, 0.156) }, u_zoom: { value: 1.0 }, u_pan: { 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, vertexShader: vertexShader, fragmentShader: fragmentShader }); // Mesh mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Event Listeners window.addEventListener('resize', onWindowResize, false); window.addEventListener('keydown', onKeyDown, false); renderer.domElement.addEventListener('mousedown', onMouseDown, false); renderer.domElement.addEventListener('mouseup', onMouseUp, false); renderer.domElement.addEventListener('mousemove', onMouseMove, false); renderer.domElement.addEventListener('wheel', onMouseWheel, false); } // --- EVENT HANDLERS --- function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.x = window.innerWidth; uniforms.u_resolution.value.y = window.innerHeight; } function onKeyDown(event) { if (event.code === 'Space') { isPlaying = !isPlaying; document.getElementById('info').style.display = isPlaying ? 'block' : 'none'; } } function onMouseDown(event) { isDragging = true; previousMousePosition.x = event.clientX; previousMousePosition.y = event.clientY; } function onMouseUp() { isDragging = false; } function onMouseMove(event) { if (!isDragging) return; const deltaX = event.clientX - previousMousePosition.x; const deltaY = event.clientY - previousMousePosition.y; // Pan sensitivity decreases as we zoom in const panSpeed = 2.0 / uniforms.u_zoom.value; uniforms.u_pan.value.x -= deltaX * (panSpeed / window.innerWidth); uniforms.u_pan.value.y += deltaY * (panSpeed / window.innerHeight); previousMousePosition.x = event.clientX; previousMousePosition.y = event.clientY; } function onMouseWheel(event) { // Prevent page scrolling event.preventDefault(); const zoomFactor = 0.95; const oldZoom = uniforms.u_zoom.value; let newZoom; if (event.deltaY < 0) { // Zoom in newZoom = oldZoom / zoomFactor; } else { // Zoom out newZoom = oldZoom * zoomFactor; } // Implement zoom-to-cursor const mouseCoord = new THREE.Vector2( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1 ); const aspectRatio = window.innerWidth / window.innerHeight; const coordBeforeZoom = new THREE.Vector2( mouseCoord.x * aspectRatio / oldZoom + uniforms.u_pan.value.x, mouseCoord.y / oldZoom + uniforms.u_pan.value.y ); const coordAfterZoom = new THREE.Vector2( mouseCoord.x * aspectRatio / newZoom + uniforms.u_pan.value.x, mouseCoord.y / newZoom + uniforms.u_pan.value.y ); // Adjust pan to keep the point under the cursor stationary uniforms.u_pan.value.x += coordBeforeZoom.x - coordAfterZoom.x; uniforms.u_pan.value.y += coordBeforeZoom.y - coordAfterZoom.y; uniforms.u_zoom.value = newZoom; } // --- ANIMATION LOOP --- function animate() { requestAnimationFrame(animate); if (isPlaying) { // Update time uniform const elapsedTime = clock.getElapsedTime(); uniforms.u_time.value = elapsedTime; // Rotate the complex constant 'c' in a circle for the morphing effect const radius = 0.7885; const speed = 0.15; // Moderate pace uniforms.u_c.value.x = radius * Math.cos(speed * elapsedTime); uniforms.u_c.value.y = radius * Math.sin(speed * elapsedTime); } renderer.render(scene, camera); } // --- START --- init(); animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/three@0.157.0/build/three.min.js"></script> <script> const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const uniforms = { resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, center: { value: new THREE.Vector2(0, 0) }, halfExtent: { value: 1.5 }, time: { value: 0 } }; const vs = ` void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fs = ` uniform vec2 resolution; uniform vec2 center; uniform float halfExtent; uniform float time; void main() { vec2 p = 2.0 * (gl_FragCoord.xy / resolution.xy) - 1.0; p.x *= resolution.x / resolution.y; vec2 z = center + p * halfExtent; float r = 0.7885; vec2 c = r * vec2(cos(time), sin(time)); const int maxIter = 1024; vec2 zn = z; int iter = 0; float bailout2 = 4.0; for (int i = 0; i < maxIter; i++) { if (dot(zn, zn) > bailout2) break; float xnew = zn.x * zn.x - zn.y * zn.y + c.x; float ynew = 2.0 * zn.x * zn.y + c.y; zn = vec2(xnew, ynew); iter = i; } vec3 col; vec3 indigo = vec3(0.294, 0.0, 0.51); if (iter == maxIter) { col = indigo; } else { float norm = dot(zn, zn); float mu = float(iter) + 1.0 - log(log(sqrt(norm))) / log(2.0); float t = mu / 100.0; if (t > 1.0) t = 1.0; vec3 colors[5]; colors[0] = vec3(1.0, 0.843, 0.0); colors[1] = vec3(1.0, 0.5, 0.0); colors[2] = vec3(1.0, 0.0, 1.0); colors[3] = vec3(0.5, 0.0, 1.0); colors[4] = indigo; float seg = t * 4.0; int i = int(floor(seg)); float f = fract(seg); if (i >= 4) col = colors[4]; else col = mix(colors[i], colors[i + 1], f); } gl_FragColor = vec4(col, 1.0); } `; const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vs, fragmentShader: fs }); const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); scene.add(mesh); window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.resolution.value.set(window.innerWidth, window.innerHeight); }); let isPlaying = true; let elapsed = 0; let last = performance.now(); const speed = 0.5; function animate(now) { requestAnimationFrame(animate); const delta = (now - last) / 1000; last = now; if (isPlaying) elapsed += delta; uniforms.time.value = elapsed * speed; renderer.render(scene, camera); } animate(performance.now()); window.addEventListener('keydown', (e) => { if (e.code === 'Space') { isPlaying = !isPlaying; e.preventDefault(); } }); let isDragging = false; let startX, startY; let startCenter = new THREE.Vector2(); window.addEventListener('mousedown', (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; startCenter.copy(uniforms.center.value); }); window.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const width = renderer.domElement.clientWidth; const height = renderer.domElement.clientHeight; const fullWidth = 2 * (width / height) * uniforms.halfExtent.value; const fullHeight = 2 * uniforms.halfExtent.value; const newCenterX = startCenter.x - (dx / width) * fullWidth; const newCenterY = startCenter.y + (dy / height) * fullHeight; uniforms.center.value.set(newCenterX, newCenterY); }); window.addEventListener('mouseup', () => isDragging = false); window.addEventListener('mouseleave', () => isDragging = false); window.addEventListener('wheel', (e) => { e.preventDefault(); const width = renderer.domElement.clientWidth; const height = renderer.domElement.clientHeight; const mx = (e.clientX / width) * 2 - 1; const my = 1 - 2 * (e.clientY / height); const aspect = width / height; const px = mx * aspect; const py = my; const pointX = uniforms.center.value.x + px * uniforms.halfExtent.value; const pointY = uniforms.center.value.y + py * uniforms.halfExtent.value; const factor = Math.exp(-e.deltaY * 0.001); const oldHalf = uniforms.halfExtent.value; const newHalf = oldHalf / factor; const ratio = newHalf / oldHalf; const newCenterX = uniforms.center.value.x * ratio + pointX * (1 - ratio); const newCenterY = uniforms.center.value.y * ratio + pointY * (1 - ratio); uniforms.center.value.set(newCenterX, newCenterY); uniforms.halfExtent.value = newHalf; }, { passive: false }); let touchStartDist = 0; let touchStartMid = new THREE.Vector2(); let touchStartCenter = new THREE.Vector2(); let touchStartHalf = 0; let isPinching = false; window.addEventListener('touchstart', (e) => { e.preventDefault(); if (e.touches.length === 1) { isDragging = true; startX = e.touches[0].clientX; startY = e.touches[0].clientY; startCenter.copy(uniforms.center.value); } else if (e.touches.length === 2) { isPinching = true; const tx1 = e.touches[0].clientX; const ty1 = e.touches[0].clientY; const tx2 = e.touches[1].clientX; const ty2 = e.touches[1].clientY; touchStartDist = Math.hypot(tx1 - tx2, ty1 - ty2); touchStartMid.set((tx1 + tx2) / 2, (ty1 + ty2) / 2); touchStartCenter.copy(uniforms.center.value); touchStartHalf = uniforms.halfExtent.value; } }, { passive: false }); window.addEventListener('touchmove', (e) => { e.preventDefault(); if (e.touches.length === 1 && isDragging) { const dx = e.touches[0].clientX - startX; const dy = e.touches[0].clientY - startY; const width = renderer.domElement.clientWidth; const height = renderer.domElement.clientHeight; const fullWidth = 2 * (width / height) * uniforms.halfExtent.value; const fullHeight = 2 * uniforms.halfExtent.value; const newCenterX = startCenter.x - (dx / width) * fullWidth; const newCenterY = startCenter.y + (dy / height) * fullHeight; uniforms.center.value.set(newCenterX, newCenterY); } else if (e.touches.length === 2 && isPinching) { const tx1 = e.touches[0].clientX; const ty1 = e.touches[0].clientY; const tx2 = e.touches[1].clientX; const ty2 = e.touches[1].clientY; const currentDist = Math.hypot(tx1 - tx2, ty1 - ty2); const currentMidX = (tx1 + tx2) / 2; const currentMidY = (ty1 + ty2) / 2; const factor = currentDist / touchStartDist; const newHalf = touchStartHalf / factor; const width = renderer.domElement.clientWidth; const height = renderer.domElement.clientHeight; const mx = (currentMidX / width) * 2 - 1; const my = 1 - 2 * (currentMidY / height); const aspect = width / height; const px = mx * aspect; const py = my; const pointX = touchStartCenter.x + px * touchStartHalf; const pointY = touchStartCenter.y + py * touchStartHalf; const ratio = newHalf / touchStartHalf; const newCenterX = touchStartCenter.x * ratio + pointX * (1 - ratio); const newCenterY = touchStartCenter.y * ratio + pointY * (1 - ratio); uniforms.center.value.set(newCenterX, newCenterY); uniforms.halfExtent.value = newHalf; } }, { passive: false }); window.addEventListener('touchend', () => { isDragging = false; isPinching = false; }); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Three.js Morphing Julia Set Shader</title> <style> html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; background: #000; } body { touch-action: none; } canvas { display: block; width: 100vw; height: 100vh; } </style> </head> <body> <script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script> <script id="vertexShader" type="x-shader/x-vertex"> varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4( position, 1.0 ); } </script> <script id="fragmentShader" type="x-shader/x-fragment"> precision highp float; varying vec2 vUv; uniform float u_time; uniform vec2 u_pan; uniform float u_zoom; uniform vec2 u_resolution; uniform bool u_paused; // Color palette by smoothstep blending between As: gold, orange, magenta, purple, indigo vec3 palette(float t) { // Four points: gold, orange, magenta, indigo vec3 c1 = vec3(1.0, 0.85, 0.07); // Golden Yellow vec3 c2 = vec3(1.0, 0.35, 0.0); // Orange vec3 c3 = vec3(0.95, 0.0, 0.75); // Magenta vec3 c4 = vec3(0.47, 0.0, 0.78); // Purple vec3 c5 = vec3(0.09, 0.07, 0.31); // Deep Indigo float s = clamp(t, 0.0, 1.0); if (s < 0.25) { float l = smoothstep(0.0, 0.25, s); return mix(c1, c2, l); } else if (s < 0.5) { float l = smoothstep(0.25, 0.5, s); return mix(c2, c3, l); } else if (s < 0.75) { float l = smoothstep(0.5, 0.75, s); return mix(c3, c4, l); } else { float l = smoothstep(0.75, 1.0, s); return mix(c4, c5, l); } } // Return smooth "escape" for color mapping float julia(vec2 z, vec2 c, int maxIt, out float smoothIter) { float i = 0.0; for (int n = 0; n < 256; n++) { if (n >= maxIt) break; z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c; if (dot(z, z) > 16.0) { i = float(n); break; } } // To avoid flat contours, estimate fractional iteration ("smooth" coloring) if (i < float(maxIt)) { float zn = log(dot(z,z)) / 2.0; float nu = log(zn / log(2.0)) / log(2.0); smoothIter = i + 1.0 - nu; } else { smoothIter = float(maxIt); } return i; } void main() { // Normalized screen coordinates centered at (0,0), screen fit vec2 uv = vUv; float aspect = u_resolution.x / u_resolution.y; vec2 p = (uv - 0.5) * vec2(aspect,1.0); // Pan/zoom p = (p + u_pan) / u_zoom; // Animate c: moderate, steady pace rotating float anim_time = u_paused ? 0.0 : u_time; float theta = anim_time * 0.25; // Keep c on a ring in (-1,1)x(-1,1) float r = mix(0.6, 0.85, 0.5+0.5*sin(0.7*anim_time)); vec2 c = r * vec2(cos(theta), sin(theta)); // Compute Julia set escape float smoothIter; int maxIter = 180; julia(p, c, maxIter, smoothIter); // Map to [0,1] for coloring float normed = smoothIter / float(maxIter); normed = pow(normed, 0.75); // Enhance gradients // Color gradient vec3 color = palette(normed); // Enhance inner fractal detail float edge = smoothstep(0.98, 1.0, normed); color = mix(color, vec3(0.03,0.03,0.1), edge); gl_FragColor = vec4(color, 1.0); } </script> <script> let renderer, scene, camera, mesh, uniforms; let width = window.innerWidth, height = window.innerHeight; let dragging = false, last = {x:0, y:0}, pan = {x: 0, y: 0}; let zoom = 1.0; let needsResize = false; let paused = false; let time = 0, lastTime = performance.now(); function init() { renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setSize(width, height, false); renderer.setPixelRatio(window.devicePixelRatio || 1); document.body.appendChild(renderer.domElement); scene = new THREE.Scene(); camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1); uniforms = { u_time: { value: 0.0 }, u_pan: { value: new THREE.Vector2(0,0)}, u_zoom: { value: zoom }, u_resolution: { value: new THREE.Vector2(width,height) }, u_paused: { value: paused } }; let mat = new THREE.ShaderMaterial({ vertexShader: document.getElementById("vertexShader").textContent, fragmentShader: document.getElementById("fragmentShader").textContent, uniforms: uniforms }); let geo = new THREE.PlaneGeometry(2,2); mesh = new THREE.Mesh(geo, mat); scene.add(mesh); window.addEventListener('resize', onResize); // Mouse controls renderer.domElement.addEventListener('mousedown', onDown); renderer.domElement.addEventListener('mousemove', onMove); renderer.domElement.addEventListener('mouseup', onUp); renderer.domElement.addEventListener('mouseleave', onUp); // Touch controls renderer.domElement.addEventListener('touchstart', onTouchStart, {passive:false}); renderer.domElement.addEventListener('touchmove', onTouchMove, {passive:false}); renderer.domElement.addEventListener('touchend', onTouchEnd, {passive:false}); // Zoom renderer.domElement.addEventListener('wheel', function(e) { e.preventDefault(); let delta = e.deltaY<0 ? 1.09 : (1/1.09); let mx = (e.offsetX / width) * 2 -1; let my = (e.offsetY / height) * 2 -1; mx *= width/height; let cursor_world = { x: (mx + pan.x) / zoom, y: (my + pan.y) / zoom }; zoom *= delta; // Keep cursor focused pan.x = cursor_world.x*zoom - mx; pan.y = cursor_world.y*zoom - my; uniforms.u_zoom.value = zoom; uniforms.u_pan.value.set(pan.x, pan.y); }, {passive: false}); // Keyboard window.addEventListener('keydown', function(e){ if (e.code === "Space") { paused = !paused; uniforms.u_paused.value = paused; } }); } function getPanSpeed() { // Speed sensitive to zoom for intuitive drag return 2.5 / zoom; } function onDown(e) { dragging = true; last.x = e.clientX; last.y = e.clientY; } function onMove(e) { if (!dragging) return; let dx = (e.clientX - last.x) / height * getPanSpeed(); let dy = (e.clientY - last.y) / height * getPanSpeed(); last.x = e.clientX; last.y = e.clientY; pan.x += dx; pan.y -= dy; uniforms.u_pan.value.set(pan.x, pan.y); } function onUp(e) { dragging = false; } // Touch support: pan and pinch-zoom let isTouching = false, lastTouchDist = 0, lastTouchMid = null; function getTouchPos(touch) { let rect = renderer.domElement.getBoundingClientRect(); let x = (touch.clientX - rect.left) / rect.width * width; let y = (touch.clientY - rect.top) / rect.height * height; return {x, y}; } function distance(a, b) { let dx = a.x-b.x, dy = a.y-b.y; return Math.sqrt(dx*dx + dy*dy); } function midpoint(a, b) { return { x: (a.x+b.x)/2, y: (a.y+b.y)/2 }; } function onTouchStart(e) { if (e.touches.length === 1) { isTouching = true; let t = getTouchPos(e.touches[0]); last.x = t.x; last.y = t.y; } else if (e.touches.length === 2) { isTouching = true; let t1 = getTouchPos(e.touches[0]); let t2 = getTouchPos(e.touches[1]); lastTouchDist = distance(t1, t2); lastTouchMid = midpoint(t1, t2); } e.preventDefault(); } function onTouchMove(e) { if (isTouching) { if (e.touches.length === 1) { let t = getTouchPos(e.touches[0]); let dx = (t.x - last.x) / height * getPanSpeed(); let dy = (t.y - last.y) / height * getPanSpeed(); last.x = t.x; last.y = t.y; pan.x += dx; pan.y -= dy; uniforms.u_pan.value.set(pan.x, pan.y); } else if (e.touches.length === 2) { let t1 = getTouchPos(e.touches[0]); let t2 = getTouchPos(e.touches[1]); let mid = midpoint(t1, t2); let d = distance(t1, t2); if (lastTouchDist > 0.0) { let delta = d / lastTouchDist; let mx = (mid.x / width) * 2 - 1; let my = (mid.y / height) * 2 - 1; mx *= width/height; let cursor_world = { x: (mx + pan.x) / zoom, y: (my + pan.y) / zoom }; zoom *= delta; // Keep pinch center in place pan.x = cursor_world.x*zoom - mx; pan.y = cursor_world.y*zoom - my; uniforms.u_zoom.value = zoom; uniforms.u_pan.value.set(pan.x, pan.y); } lastTouchDist = d; lastTouchMid = mid; } } e.preventDefault(); } function onTouchEnd(e) { isTouching = false; lastTouchDist = 0; } function onResize() { needsResize = true; } function doResize() { width = window.innerWidth; height = window.innerHeight; renderer.setSize(width, height, false); uniforms.u_resolution.value.set(width, height); } function animate() { requestAnimationFrame(animate); if (needsResize) { needsResize = false; doResize(); } let now = performance.now(); let dt = Math.min(0.05, (now - lastTime) * 0.001); // Clamp step lastTime = now; if (!paused) time += dt; uniforms.u_time.value = time; renderer.render(scene, camera); } init(); animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Fullscreen Morphing Julia Set</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script src="https://unpkg.com/three@0.153.0/build/three.min.js"></script> <script> // Scene, camera, renderer const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const renderer = new THREE.WebGLRenderer({ antialias: true }); document.body.appendChild(renderer.domElement); // Uniforms const uniforms = { time: { value: 0.0 }, resolution: { value: new THREE.Vector2() }, zoom: { value: 1.5 }, offset: { value: new THREE.Vector2(0.0, 0.0) } }; // Fullscreen quad const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` precision highp float; uniform vec2 resolution; uniform float time; uniform float zoom; uniform vec2 offset; // Color palette interpolation vec3 palette(float t){ vec3 a = vec3(1.0, 0.8, 0.2); // gold vec3 b = vec3(1.0, 0.5, 0.0); // orange vec3 c = vec3(1.0, 0.0, 1.0); // magenta vec3 d = vec3(0.6, 0.0, 0.6); // purple vec3 e = vec3(0.3, 0.0, 0.5); // indigo if(t < 0.25) return mix(a, b, t/0.25); else if(t < 0.5) return mix(b, c, (t-0.25)/0.25); else if(t < 0.75) return mix(c, d, (t-0.5)/0.25); else return mix(d, e, (t-0.75)/0.25); } void main(){ vec2 uv = (gl_FragCoord.xy / resolution.xy) * 2.0 - 1.0; uv.x *= resolution.x / resolution.y; // complex plane coords vec2 z = uv * zoom + offset; // rotating constant float radius = 0.7885; float ang = time * 0.2; vec2 c = radius * vec2(cos(ang), sin(ang)); const int MAX_IT = 300; int i; float mu = 0.0; for(i = 0; i < MAX_IT; 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) { // smooth iteration count float log_zn = log(dot(z,z)) / 2.0; float nu = log(log_zn / log(2.0)) / log(2.0); mu = float(i) + 1.0 - nu; break; } } float t = mu < 1.0 ? 0.0 : mu / float(MAX_IT); vec3 col = palette(t); gl_FragColor = vec4(col, 1.0); } ` }); const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); scene.add(quad); // Resize handler function onResize() { const w = window.innerWidth, h = window.innerHeight; renderer.setSize(w, h); uniforms.resolution.value.set(w, h); } window.addEventListener('resize', onResize); onResize(); // Interaction state let isPaused = false; let startTime = performance.now(); let pausedTime = 0; // Pan let isDragging = false; let lastX = 0, lastY = 0; renderer.domElement.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; }); window.addEventListener('mousemove', e => { if(!isDragging) return; const dx = (e.clientX - lastX) / window.innerWidth * 2 * uniforms.zoom.value * (window.innerWidth/window.innerHeight); const dy = -(e.clientY - lastY) / window.innerHeight * 2 * uniforms.zoom.value; uniforms.offset.value.x -= dx; uniforms.offset.value.y -= dy; lastX = e.clientX; lastY = e.clientY; }); window.addEventListener('mouseup', () => { isDragging = false; }); // Wheel zoom renderer.domElement.addEventListener('wheel', e => { e.preventDefault(); const factor = e.deltaY > 0 ? 1.1 : 0.9; // zoom around pointer const nx = (e.clientX / window.innerWidth) * 2 - 1; const ny = 1 - (e.clientY / window.innerHeight) * 2; const before = new THREE.Vector2( nx * (window.innerWidth/window.innerHeight) * uniforms.zoom.value + uniforms.offset.value.x, ny * uniforms.zoom.value + uniforms.offset.value.y ); uniforms.zoom.value *= factor; const after = new THREE.Vector2( nx * (window.innerWidth/window.innerHeight) * uniforms.zoom.value + uniforms.offset.value.x, ny * uniforms.zoom.value + uniforms.offset.value.y ); uniforms.offset.value.sub(after).add(before); }, { passive: false }); // Touch pan & pinch let touchMode = null; let initDist = 0, initZoom = 1.5; let initOff = new THREE.Vector2(); let initCenter = new THREE.Vector2(); renderer.domElement.addEventListener('touchstart', e => { if(e.touches.length === 1) { touchMode = 'pan'; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; } else if(e.touches.length === 2) { touchMode = 'pinch'; const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; initDist = Math.hypot(dx, dy); initZoom = uniforms.zoom.value; initOff.copy(uniforms.offset.value); // center const cx = (e.touches[0].clientX + e.touches[1].clientX)/2; const cy = (e.touches[0].clientY + e.touches[1].clientY)/2; initCenter.set(cx, cy); } }, { passive: false }); renderer.domElement.addEventListener('touchmove', e => { e.preventDefault(); if(touchMode === 'pan' && e.touches.length===1) { const x = e.touches[0].clientX, y = e.touches[0].clientY; const dx = (x - lastX)/window.innerWidth*2*uniforms.zoom.value*(window.innerWidth/window.innerHeight); const dy = -(y - lastY)/window.innerHeight*2*uniforms.zoom.value; uniforms.offset.value.x -= dx; uniforms.offset.value.y -= dy; lastX = x; lastY = y; } else if(touchMode === 'pinch' && e.touches.length===2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const dist = Math.hypot(dx, dy); const factor = dist / initDist; // zoom around initial center const nx = (initCenter.x / window.innerWidth)*2 -1; const ny = 1 - (initCenter.y / window.innerHeight)*2; const before = new THREE.Vector2( nx*(window.innerWidth/window.innerHeight)*uniforms.zoom.value + uniforms.offset.value.x, ny*uniforms.zoom.value + uniforms.offset.value.y ); uniforms.zoom.value = initZoom * factor; const after = new THREE.Vector2( nx*(window.innerWidth/window.innerHeight)*uniforms.zoom.value + uniforms.offset.value.x, ny*uniforms.zoom.value + uniforms.offset.value.y ); uniforms.offset.value.sub(after).add(before); } }, { passive: false }); renderer.domElement.addEventListener('touchend', e => { if(e.touches.length===0) touchMode = null; else if(e.touches.length===1) { touchMode = 'pan'; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; } }); // Spacebar toggle window.addEventListener('keydown', e => { if(e.code === 'Space') { isPaused = !isPaused; if(isPaused) pausedTime = performance.now(); else startTime += performance.now() - pausedTime; } }); // Render loop function animate() { requestAnimationFrame(animate); const now = performance.now(); uniforms.time.value = (isPaused ? pausedTime : now) - startTime; uniforms.time.value *= 0.001; // seconds renderer.render(scene, camera); } animate(); </script> </body> </html> ```
# Interactive Julia Set Fractal Animation with Three.js I'll create a full-screen interactive Julia set fractal with smooth color transitions and animation controls. Here's the complete solution: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Morphing 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; top: 20px; left: 20px; color: rgba(255, 255, 255, 0.7); background: rgba(0, 0, 0, 0.5); padding: 15px; border-radius: 10px; font-size: 14px; max-width: 300px; backdrop-filter: blur(5px); } #info h1 { margin: 0 0 10px 0; font-size: 22px; color: #ffcc00; } #info ul { padding-left: 20px; margin: 10px 0; } #info li { margin-bottom: 8px; line-height: 1.4; } #status { position: absolute; bottom: 20px; right: 20px; color: white; background: rgba(0, 0, 0, 0.5); padding: 8px 15px; border-radius: 20px; font-size: 14px; backdrop-filter: blur(5px); } .pulse { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #0f0; margin-right: 8px; box-shadow: 0 0 5px #0f0; animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } } </style> </head> <body> <div id="info"> <h1>Julia Set Fractal Explorer</h1> <p>This interactive visualization shows a continuously morphing Julia set fractal with smooth color transitions.</p> <ul> <li><strong>Drag</strong> to pan around the fractal</li> <li><strong>Scroll</strong> to zoom in and out</li> <li><strong>Spacebar</strong> to pause/resume animation</li> <li><strong>Double-click</strong> to reset view</li> </ul> <p>Colors transition from golden yellow through orange, magenta, and purple to deep indigo based on iteration count.</p> </div> <div id="status"> <span class="pulse"></span> <span id="status-text">Playing</span> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Main variables let scene, camera, renderer, mesh; let uniforms, isPlaying = true; let offset = { x: 0, y: 0 }; let scale = 2.5; let mouseDown = false; let lastMouse = { x: 0, y: 0 }; let time = 0; // 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, -1, 1 ); scene.add(camera); // Create renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); // Create shader material uniforms = { u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_offset: { value: new THREE.Vector2(offset.x, offset.y) }, u_scale: { value: scale }, u_time: { value: 0 }, u_aspect: { value: aspect } }; const material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 u_resolution; uniform vec2 u_offset; uniform float u_scale; uniform float u_time; uniform float u_aspect; // Smooth coloring function vec3 palette(float t) { vec3 a = vec3(0.95, 0.65, 0.15); // Golden yellow vec3 b = vec3(0.95, 0.35, 0.0); // Orange vec3 c = vec3(0.9, 0.1, 0.5); // Magenta vec3 d = vec3(0.5, 0.0, 0.5); // Purple vec3 e = vec3(0.2, 0.0, 0.3); // Deep indigo // Blend between colors based on t if (t < 0.25) { return mix(a, b, t * 4.0); } else if (t < 0.5) { return mix(b, c, (t - 0.25) * 4.0); } else if (t < 0.75) { return mix(c, d, (t - 0.5) * 4.0); } else { return mix(d, e, (t - 0.75) * 4.0); } } void main() { // Normalize coordinates with aspect ratio vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x); uv *= u_scale; uv += u_offset; // Rotating complex constant c float angle = u_time * 0.1; vec2 c = vec2(0.7885 * cos(angle), 0.7885 * sin(angle)); vec2 z = uv; float iterations = 0.0; const float max_iter = 200.0; // Julia set iteration for (float i = 0.0; i < max_iter; i++) { if (dot(z, z) > 4.0) break; // Complex multiplication: z = z^2 + c z = vec2( z.x * z.x - z.y * z.y, 2.0 * z.x * z.y ) + c; iterations++; } // Smooth coloring float t = iterations / max_iter; vec3 color = palette(t); // Add some glow for depth color += 0.1 * vec3(t * t * t); gl_FragColor = vec4(color, 1.0); } ` }); // Create fullscreen quad const geometry = new THREE.PlaneGeometry(2, 2); mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Event listeners window.addEventListener('resize', onWindowResize); document.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); document.addEventListener('wheel', onMouseWheel); document.addEventListener('keydown', onKeyDown); document.addEventListener('dblclick', onDoubleClick); } // Handle window resize function onWindowResize() { const aspect = window.innerWidth / window.innerHeight; camera.left = -aspect; camera.right = aspect; camera.top = 1; camera.bottom = -1; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); uniforms.u_resolution.value.set(window.innerWidth, window.innerHeight); uniforms.u_aspect.value = aspect; } // Mouse event handlers function onMouseDown(event) { mouseDown = true; lastMouse.x = event.clientX; lastMouse.y = event.clientY; } function onMouseMove(event) { if (!mouseDown) return; const dx = (event.clientX - lastMouse.x) / window.innerWidth; const dy = (event.clientY - lastMouse.y) / window.innerHeight; offset.x -= dx * scale; offset.y += dy * scale; uniforms.u_offset.value.set(offset.x, offset.y); lastMouse.x = event.clientX; lastMouse.y = event.clientY; } function onMouseUp() { mouseDown = false; } function onMouseWheel(event) { event.preventDefault(); const zoomSpeed = 0.1; const zoomFactor = 1.0 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed); // Adjust scale and offset for zooming toward mouse position const rect = renderer.domElement.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; const normX = (mouseX / window.innerWidth - 0.5) * 2.0 * (window.innerWidth / window.innerHeight); const normY = (1.0 - mouseY / window.innerHeight - 0.5) * 2.0; const prevScale = scale; scale *= zoomFactor; scale = Math.max(0.1, Math.min(scale, 100.0)); offset.x += (normX * (prevScale - scale)); offset.y += (normY * (prevScale - scale)); uniforms.u_scale.value = scale; uniforms.u_offset.value.set(offset.x, offset.y); } function onKeyDown(event) { if (event.code === 'Space') { isPlaying = !isPlaying; document.getElementById('status-text').textContent = isPlaying ? 'Playing' : 'Paused'; } } function onDoubleClick() { // Reset view offset.x = 0; offset.y = 0; scale = 2.5; uniforms.u_offset.value.set(offset.x, offset.y); uniforms.u_scale.value = scale; } // Animation loop function animate() { requestAnimationFrame(animate); if (isPlaying) { time += 0.01; uniforms.u_time.value = time; } renderer.render(scene, camera); } // Initialize and start animation init(); animate(); </script> </body> </html> ``` ## Features Implemented 1. **Morphing Julia Set Fractal**: - Computes the Julia set with 200 iterations in the fragment shader - Complex constant rotates at a steady pace creating the morphing effect 2. **Smooth Color Transitions**: - Colors transition from golden yellow → orange → magenta → purple → deep indigo - Smooth coloring based on iteration count for visual appeal 3. **Interactive Controls**: - Click and drag to pan around the fractal - Mouse wheel to zoom in/out (zooms toward cursor position) - Space bar to toggle animation play/pause - Double-click to reset view 4. **Visual Design**: - Full-screen rendering with responsive design - Informative overlay with controls explanation - Status indicator showing animation state - Smooth animations and transitions The fractal constantly evolves as the complex constant rotates, creating mesmerizing patterns. The color transitions highlight the intricate details of the Julia set at different iteration depths.
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Three.js – Morphing Julia Set</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> /* ---------- Basic Three.js setup ---------- */ const renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setPixelRatio( window.devicePixelRatio ); document.body.appendChild( renderer.domElement ); const camera = new THREE.OrthographicCamera( -1, 1, 1, -1, 0, 1 ); // just a screen-space quad const scene = new THREE.Scene(); /* ---------- Shader material ---------- */ const MAX_ITER = 220; // iterations in the fragment shader const ROT_SPEED = 0.35; // radians per second for the Julia constant const uniforms = { u_resolution : { value: new THREE.Vector2() }, u_time : { value: 0.0 }, u_c : { value: new THREE.Vector2() }, u_zoom : { value: 1.0 }, u_offset : { value: new THREE.Vector2() }, }; const fragmentShader = ` uniform vec2 u_resolution; uniform float u_time; uniform vec2 u_c; uniform float u_zoom; uniform vec2 u_offset; const int MAX_ITER = ${MAX_ITER}; vec3 palette(float t){ // key colors vec3 c0 = vec3(1.0,0.8,0.0); // golden yellow vec3 c1 = vec3(1.0,0.5,0.0); // orange vec3 c2 = vec3(1.0,0.0,0.5); // magenta vec3 c3 = vec3(0.6,0.0,1.0); // purple vec3 c4 = vec3(0.2,0.0,0.5); // indigo t = clamp(t,0.0,1.0); float x = t * 4.0; float f = fract(x); if(x < 1.0) return mix(c0, c1, f); else if(x < 2.0) return mix(c1, c2, f); else if(x < 3.0) return mix(c2, c3, f); else return mix(c3, c4, f); } void main(){ vec2 uv = gl_FragCoord.xy - 0.5 * u_resolution; float scale = min(u_resolution.x, u_resolution.y); vec2 z = (uv / scale) * u_zoom + u_offset; int i; vec2 c = u_c; for(i = 0; i < MAX_ITER; i++){ // z = z^2 + c in complex plane z = vec2( z.x*z.x - z.y*z.y, 2.0*z.x*z.y ) + c; if(dot(z,z) > 16.0) break; } float it = float(i); if(i < MAX_ITER){ // smooth iteration count float zn = dot(z,z); it = it + 1.0 - log2(log2(zn)) + 1.0; } float t = it / float(MAX_ITER); vec3 col = palette(t); if(i == MAX_ITER) col = vec3(0.0); // interior = black gl_FragColor = vec4(col,1.0); } `; const material = new THREE.ShaderMaterial({ uniforms, fragmentShader, vertexShader: ` void main(){ gl_Position = vec4( position, 1.0 ); } ` }); const quad = new THREE.Mesh( new THREE.PlaneGeometry(2,2), material ); scene.add( quad ); /* ---------- Handle resizing ---------- */ function resize(){ renderer.setSize( window.innerWidth, window.innerHeight ); uniforms.u_resolution.value.set( window.innerWidth, window.innerHeight ); } window.addEventListener('resize', resize); resize(); /* ---------- Interaction: panning & zooming ---------- */ let isDragging = false; let dragStart = new THREE.Vector2(); let offsetStart= new THREE.Vector2(); renderer.domElement.addEventListener('pointerdown', e=>{ isDragging = true; dragStart.set( e.clientX, e.clientY ); offsetStart.copy( uniforms.u_offset.value ); }); window.addEventListener('pointerup', ()=> isDragging = false ); window.addEventListener('pointermove', e=>{ if(!isDragging) return; const dx = e.clientX - dragStart.x; const dy = e.clientY - dragStart.y; const scale = Math.min( window.innerWidth, window.innerHeight ); const zoom = uniforms.u_zoom.value; uniforms.u_offset.value.set( offsetStart.x - (dx / scale) * zoom, offsetStart.y + (dy / scale) * zoom ); }); function zoomAt(pointX, pointY, zoomFactor){ const res = uniforms.u_resolution.value; const zoom = uniforms.u_zoom.value; const scale = Math.min( res.x, res.y ); // convert screen point to fractal coord const before = new THREE.Vector2( (pointX - 0.5*res.x) / scale * zoom + uniforms.u_offset.value.x, (pointY - 0.5*res.y) / scale * zoom + uniforms.u_offset.value.y ); uniforms.u_zoom.value *= zoomFactor; const zoomNew = uniforms.u_zoom.value; const after = new THREE.Vector2( (pointX - 0.5*res.x) / scale * zoomNew + uniforms.u_offset.value.x, (pointY - 0.5*res.y) / scale * zoomNew + uniforms.u_offset.value.y ); // adjust offset so the point under cursor stays fixed uniforms.u_offset.value.add( before.clone().sub(after) ); } window.addEventListener('wheel', e=>{ e.preventDefault(); const factor = e.deltaY < 0 ? 0.85 : 1.15; zoomAt( e.clientX, e.clientY, factor ); },{passive:false}); /* ---------- Animation loop + pause ---------- */ let paused = false; let lastTime = performance.now(); function animate(now){ requestAnimationFrame( animate ); if(!paused){ const dt = (now - lastTime) * 0.001; // seconds uniforms.u_time.value += dt; // rotate the Julia constant const theta = uniforms.u_time.value * ROT_SPEED; const r = 0.7885; // radius of constant uniforms.u_c.value.set( Math.cos(theta)*r, Math.sin(theta)*r ); } lastTime = now; renderer.render( scene, camera ); } requestAnimationFrame( animate ); /* ---------- Space bar pause / resume ---------- */ window.addEventListener('keydown', e=>{ if(e.code === 'Space'){ paused = !paused; } }); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Julia Set Fractal Shader</title> <style> body { margin: 0; overflow: hidden; background-color: black; } canvas { display: block; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Set up the scene, camera, and renderer 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); // Vertex shader const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; // Fragment shader const fragmentShader = ` varying vec2 vUv; uniform vec2 resolution; uniform float time; uniform vec2 c; uniform bool isPaused; uniform float zoom; uniform vec2 offset; void main() { // Convert UV coordinates to complex plane coordinates with pan and zoom vec2 uv = vUv * 2.0 - 1.0; uv.x *= resolution.x / resolution.y; // Adjust for aspect ratio // Apply zoom and offset uv = uv / zoom + offset; // Initial z value vec2 z = uv; // Julia set parameters vec2 constant = c; if (!isPaused) { // Rotate the constant over time float angle = time * 0.3; // Moderate pace constant = vec2(cos(angle) * 0.37, sin(angle) * 0.1) + c * 0.1; } // Julia set iteration float iterations = 0.0; float maxIterations = 200.0; for (float i = 0.0; i < maxIterations; i++) { iterations = i; // z = z^2 + c vec2 zSquared = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y); z = zSquared + constant; // Break if we're escaping to infinity if (dot(z, z) > 4.0) break; } // Smooth coloring float smoothColor = iterations - log(log(length(z))) / log(2.0); smoothColor = clamp(smoothColor / maxIterations, 0.0, 1.0); // Color mapping vec3 color1 = vec3(1.0, 0.84, 0.0); // Golden yellow vec3 color2 = vec3(1.0, 0.5, 0.0); // Orange vec3 color3 = vec3(1.0, 0.0, 0.5); // Magenta vec3 color4 = vec3(0.5, 0.0, 0.5); // Purple vec3 color5 = vec3(0.29, 0.0, 0.51); // Deep indigo vec3 color; if (smoothColor < 0.25) { color = mix(color1, color2, smoothColor * 4.0); } else if (smoothColor < 0.5) { color = mix(color2, color3, (smoothColor - 0.25) * 4.0); } else if (smoothColor < 0.75) { color = mix(color3, color4, (smoothColor - 0.5) * 4.0); } else { color = mix(color4, color5, (smoothColor - 0.75) * 4.0); } // If we're at max iterations, color is black if (iterations >= maxIterations - 1.0) { color = vec3(0.0, 0.0, 0.0); } gl_FragColor = vec4(color, 1.0); } `; // Create a shader material const uniforms = { resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, time: { value: 0.0 }, c: { value: new THREE.Vector2(-0.7, 0.27015) }, // Initial Julia set constant isPaused: { value: false }, zoom: { value: 1.0 }, offset: { value: new THREE.Vector2(0, 0) } }; const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms }); // Create a plane that fills the screen const geometry = new THREE.PlaneGeometry(2, 2); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Pan and zoom variables let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; // Mouse down event renderer.domElement.addEventListener('mousedown', (event) => { isDragging = true; previousMousePosition = { x: event.clientX, y: event.clientY }; }); // Mouse move event renderer.domElement.addEventListener('mousemove', (event) => { if (isDragging) { const deltaMove = { x: event.clientX - previousMousePosition.x, y: event.clientY - previousMousePosition.y }; // Update offset uniforms.offset.value.x += deltaMove.x * 0.01 / uniforms.zoom.value; uniforms.offset.value.y -= deltaMove.y * 0.01 / uniforms.zoom.value; previousMousePosition = { x: event.clientX, y: event.clientY }; } }); // Mouse up event renderer.domElement.addEventListener('mouseup', () => { isDragging = false; }); // Mouse leave event renderer.domElement.addEventListener('mouseleave', () => { isDragging = false; }); // Mouse wheel event for zooming renderer.domElement.addEventListener('wheel', (event) => { event.preventDefault(); // Adjust zoom based on wheel delta uniforms.zoom.value += event.deltaY * -0.001 * uniforms.zoom.value; // Clamp zoom to reasonable values uniforms.zoom.value = Math.min(Math.max(0.1, uniforms.zoom.value), 10); }); // Space bar to toggle play/pause window.addEventListener('keydown', (event) => { if (event.code === 'Space') { uniforms.isPaused.value = !uniforms.isPaused.value; } }); // Animation loop function animate() { requestAnimationFrame(animate); // Update time uniform if (!uniforms.isPaused.value) { uniforms.time.value += 0.01; } renderer.render(scene, camera); } // Handle window resize window.addEventListener('resize', () => { const width = window.innerWidth; const height = window.innerHeight; renderer.setSize(width, height); uniforms.resolution.value.set(width, height); }); animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Morphing Julia Set</title> <style> body { margin: 0; overflow: hidden; } canvas { display: block; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/three@0.155.0/build/three.min.js"></script> <script> // Scene setup 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); // Shader material const material = new THREE.ShaderMaterial({ fragmentShader: ` precision highp float; uniform float u_time; uniform vec2 u_resolution; uniform vec2 u_offset; uniform float u_scale; uniform bool u_isPlaying; varying vec2 vUv; vec3 getColor(float t) { vec3 color = vec3(0.0); color = mix(vec3(1.0, 0.843, 0.0), vec3(1.0, 0.647, 0.0), smoothstep(0.0, 0.25, t)); color = mix(color, vec3(1.0, 0.0, 1.0), smoothstep(0.25, 0.5, t)); color = mix(color, vec3(0.5, 0.0, 0.5), smoothstep(0.5, 0.75, t)); color = mix(color, vec3(0.298, 0.0, 0.509), smoothstep(0.75, 1.0, t)); return color; } void main() { vec2 uv = (vUv - 0.5) * u_scale + u_offset; vec2 c = vec2(0.0); if (u_isPlaying) { float angle = u_time * 0.2; c = vec2(cos(angle), sin(angle)) * 0.7885; } else { c = vec2(0.285, 0.01); } vec2 z = uv; float iter = 0.0; const int maxIter = 100; for (int i = 0; i < maxIter; 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 t = iter / float(maxIter); vec3 color = getColor(t); gl_FragColor = vec4(color, 1.0); } `, uniforms: { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_offset: { value: new THREE.Vector2(0, 0) }, u_scale: { value: 1.0 }, u_isPlaying: { value: true } } }); // Fullscreen quad const geometry = new THREE.PlaneGeometry(2, 2); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Resize handler window.addEventListener('resize', () => { const width = window.innerWidth; const height = window.innerHeight; const aspect = width / height; camera.left = -aspect; camera.right = aspect; camera.top = 1; camera.bottom = -1; camera.updateProjectionMatrix(); renderer.setSize(width, height); material.uniforms.u_resolution.value.set(width, height); }); // Controls let isDragging = false; let lastX = 0, lastY = 0; document.addEventListener('mousedown', (e) => { isDragging = true; lastX = e.clientX; lastY = e.clientY; }); document.addEventListener('mouseup', () => { isDragging = false; }); document.addEventListener('mousemove', (e) => { if (isDragging) { const dx = e.clientX - lastX; const dy = e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; const scale = material.uniforms.u_scale.value; material.uniforms.u_offset.value.x -= dx / window.innerWidth * scale; material.uniforms.u_offset.value.y += dy / window.innerHeight * scale; } }); document.addEventListener('wheel', (e) => { const factor = 1.1; if (e.deltaY < 0) { material.uniforms.u_scale.value /= factor; } else { material.uniforms.u_scale.value *= factor; } }); document.addEventListener('keydown', (e) => { if (e.code === 'Space') { material.uniforms.u_isPlaying.value = !material.uniforms.u_isPlaying.value; } }); // Animation loop let lastTime = performance.now(); function animate() { requestAnimationFrame(animate); if (material.uniforms.u_isPlaying.value) { const now = performance.now(); const delta = (now - lastTime) / 1000; material.uniforms.u_time.value += delta; lastTime = now; } renderer.render(scene, camera); } animate(); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Animated Julia Set Fractal</title> <style> body { margin: 0; overflow: hidden; background-color: #000; } canvas { display: block; } </style> </head> <body> <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.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); // Uniforms for shader const uniforms = { time: { type: "f", value: 0.0 }, resolution: { type: "v2", value: new THREE.Vector2() }, zoom: { type: "f", value: 1.0 }, center: { type: "v2", value: new THREE.Vector2(0.0, 0.0) } }; // Create shader material const material = new THREE.ShaderMaterial({ uniforms: uniforms, fragmentShader: ` uniform float time; uniform vec2 resolution; uniform float zoom; uniform vec2 center; const int maxIterations = 100; vec3 palette(float t) { vec3 a = vec3(0.966, 0.753, 0.204); // gold vec3 b = vec3(0.929, 0.286, 0.243); // orange-red vec3 c = vec3(0.694, 0.149, 0.686); // magenta-purple vec3 d = vec3(0.267, 0.039, 0.592); // indigo return a + b*cos(6.28318*(c*t + d)); } void main() { vec2 uv = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y); uv /= zoom; uv += center; // Animate Julia set constant float angle = time * 0.2; vec2 c = vec2(0.3 * cos(angle), 0.3 * sin(angle)); vec2 z = uv; int iterations = 0; for (int i = 0; i < maxIterations; i++) { z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; if (dot(z, z) > 4.0) break; iterations++; } float t = float(iterations) / float(maxIterations); vec3 color = iterations == maxIterations ? vec3(0.0) : palette(t * 0.5 + 0.3); gl_FragColor = vec4(color, 1.0); } ` }); // Create full-screen quad const geometry = new THREE.PlaneGeometry(2, 2); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Interaction variables let isDragging = false; let lastMouseX = 0; let lastMouseY = 0; let animationPlaying = true; // Event listeners window.addEventListener('resize', onWindowResize); document.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); document.addEventListener('wheel', onMouseWheel); document.addEventListener('keydown', onKeyDown); // Handle touch events for mobile document.addEventListener('touchstart', onTouchStart); document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); uniforms.resolution.value.set(window.innerWidth, window.innerHeight); } function onMouseDown(event) { isDragging = true; lastMouseX = event.clientX; lastMouseY = event.clientY; } function onMouseMove(event) { if (!isDragging) return; const deltaX = event.clientX - lastMouseX; const deltaY = event.clientY - lastMouseY; uniforms.center.value.x -= deltaX / (window.innerWidth * uniforms.zoom.value * 0.5); uniforms.center.value.y += deltaY / (window.innerHeight * uniforms.zoom.value * 0.5); lastMouseX = event.clientX; lastMouseY = event.clientY; } function onMouseUp() { isDragging = false; } function onMouseWheel(event) { event.preventDefault(); const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1; uniforms.zoom.value *= zoomFactor; } function onKeyDown(event) { if (event.code === 'Space') { animationPlaying = !animationPlaying; event.preventDefault(); } } function onTouchStart(event) { isDragging = true; lastMouseX = event.touches[0].clientX; lastMouseY = event.touches[0].clientY; event.preventDefault(); } function onTouchMove(event) { if (!isDragging) return; const deltaX = event.touches[0].clientX - lastMouseX; const deltaY = event.touches[0].clientY - lastMouseY; uniforms.center.value.x -= deltaX / (window.innerWidth * uniforms.zoom.value * 0.5); uniforms.center.value.y += deltaY / (window.innerHeight * uniforms.zoom.value * 0.5); lastMouseX = event.touches[0].clientX; lastMouseY = event.touches[0].clientY; event.preventDefault(); } function onTouchEnd() { isDragging = false; } // Initialize uniforms.resolution.value.set(window.innerWidth, window.innerHeight); let lastTime = 0; // Animation loop function animate(currentTime) { requestAnimationFrame(animate); if (animationPlaying) { const deltaTime = (currentTime - lastTime) / 1000; uniforms.time.value += deltaTime; } lastTime = currentTime; renderer.render(scene, camera); } animate(0); </script> </body> </html> ```
```html <!DOCTYPE html> <html> <head> <title>Julia Set Fractal Animation</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({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); let geometry = new THREE.PlaneGeometry(2, 2); let uniforms = { iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, iTime: { value: 0 }, c: { value: new THREE.Vector2(0.4, 0.6) }, zoom: { value: 1 }, pan: { value: new THREE.Vector2(0, 0) } }; let material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform vec2 iResolution; uniform float iTime; uniform vec2 c; uniform float zoom; uniform vec2 pan; void main() { vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy + pan) / (iResolution.y * zoom); vec2 z = uv; float maxIter = 256.0; float iter = 0.0; for (int i = 0; i < 256; i++) { if (iter >= maxIter) break; z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c; if (dot(z, z) > 4.0) { iter = float(i); break; } } vec3 col = vec3(0.0); if (iter < maxIter) { float t = iter / maxIter; col = 0.5 + 0.5 * cos(3.0 + t * 15.0 + vec3(0.0, 0.6, 1.0)); } gl_FragColor = vec4(col, 1.0); } ` }); let mesh = new THREE.Mesh(geometry, material); scene.add(mesh); let isDragging = false; let lastMousePosition = null; document.addEventListener('mousedown', (event) => { isDragging = true; lastMousePosition = new THREE.Vector2(event.clientX, event.clientY); }); document.addEventListener('mousemove', (event) => { if (isDragging) { let currentMousePosition = new THREE.Vector2(event.clientX, event.clientY); let delta = currentMousePosition.clone().sub(lastMousePosition); uniforms.pan.value.add(new THREE.Vector2(delta.x / window.innerHeight * uniforms.zoom.value, -delta.y / window.innerHeight * uniforms.zoom.value)); lastMousePosition = currentMousePosition; } }); document.addEventListener('mouseup', () => { isDragging = false; lastMousePosition = null; }); document.addEventListener('wheel', (event) => { let zoom = uniforms.zoom.value; zoom *= Math.pow(1.1, -event.deltaY); uniforms.zoom.value = zoom; }); let isPlaying = true; document.addEventListener('keydown', (event) => { if (event.code === 'Space') { isPlaying = !isPlaying; } }); function animate() { requestAnimationFrame(animate); if (isPlaying) { uniforms.iTime.value += 0.01; uniforms.c.value = new THREE.Vector2(0.4 * Math.cos(uniforms.iTime.value), 0.6 * Math.sin(uniforms.iTime.value)); } renderer.render(scene, camera); } animate(); window.addEventListener('resize', () => { renderer.setSize(window.innerWidth, window.innerHeight); uniforms.iResolution.value = new THREE.Vector2(window.innerWidth, window.innerHeight); }); </script> </body> </html> ```
```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Morphing Julia Set Fractal</title> <style> body { margin: 0; padding: 0; overflow: hidden; background: black; } canvas { display: block; } </style> </head> <body> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> <script> // Vertex shader const vertexShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; // Fragment shader for Julia set const fragmentShader = ` uniform float u_time; uniform vec2 u_resolution; uniform vec2 u_offset; uniform float u_zoom; uniform vec2 u_c; 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); } vec3 getColor(float t) { // Smooth color transition: golden yellow -> orange -> magenta -> purple -> deep indigo vec3 colors[5]; colors[0] = vec3(1.0, 0.84, 0.0); // Golden yellow colors[1] = vec3(1.0, 0.5, 0.0); // Orange colors[2] = vec3(1.0, 0.0, 1.0); // Magenta colors[3] = vec3(0.5, 0.0, 1.0); // Purple colors[4] = vec3(0.15, 0.0, 0.3); // Deep indigo t = clamp(t, 0.0, 1.0); float scaledT = t * 4.0; int index = int(floor(scaledT)); float fraction = fract(scaledT); if (index >= 3) { return mix(colors[3], colors[4], fraction); } else if (index >= 2) { return mix(colors[2], colors[3], fraction); } else if (index >= 1) { return mix(colors[1], colors[2], fraction); } else { return mix(colors[0], colors[1], fraction); } } int julia(vec2 z, vec2 c) { const int maxIterations = 256; for (int i = 0; i < maxIterations; i++) { if (dot(z, z) > 4.0) { return i; } // 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); } return maxIterations; } void main() { vec2 uv = vUv * 2.0 - 1.0; uv.x *= u_resolution.x / u_resolution.y; // Apply zoom and offset uv = uv / u_zoom + u_offset; int iterations = julia(uv, u_c); if (iterations == 256) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); } else { float t = float(iterations) / 256.0; // Apply smooth coloring t = smoothstep(0.0, 1.0, t); vec3 color = getColor(t); gl_FragColor = vec4(color, 1.0); } } `; // Scene setup let scene, camera, renderer, material, mesh; let mouse = { x: 0, y: 0, down: false }; let offset = { x: 0, y: 0 }; let zoom = 1.0; let isPlaying = true; 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 material with shader material = new THREE.ShaderMaterial({ vertexShader: vertexShader, fragmentShader: fragmentShader, uniforms: { u_time: { value: 0.0 }, u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, u_offset: { value: new THREE.Vector2(0.0, 0.0) }, u_zoom: { value: 1.0 }, u_c: { value: new THREE.Vector2(-0.4, 0.6) } } }); // Create geometry and mesh const geometry = new THREE.PlaneGeometry(2, 2); mesh = new THREE.Mesh(geometry, material); scene.add(mesh); // Add event listeners setupEventListeners(); } function setupEventListeners() { // Mouse events for panning renderer.domElement.addEventListener('mousedown', onMouseDown); renderer.domElement.addEventListener('mousemove', onMouseMove); renderer.domElement.addEventListener('mouseup', onMouseUp); renderer.domElement.addEventListener('wheel', onWheel); // Touch events for mobile renderer.domElement.addEventListener('touchstart', onTouchStart); renderer.domElement.addEventListener('touchmove', onTouchMove); renderer.domElement.addEventListener('touchend', onTouchEnd); // Keyboard events document.addEventListener('keydown', onKeyDown); // Window resize window.addEventListener('resize', onWindowResize); } function onMouseDown(event) { mouse.down = true; mouse.x = event.clientX; mouse.y = event.clientY; } function onMouseMove(event) { if (mouse.down) { const deltaX = (event.clientX - mouse.x) / window.innerWidth * 4 / zoom; const deltaY = (event.clientY - mouse.y) / window.innerHeight * 4 / zoom; offset.x -= deltaX; offset.y += deltaY; material.uniforms.u_offset.value.set(offset.x, offset.y); mouse.x = event.clientX; mouse.y = event.clientY; } } function onMouseUp(event) { mouse.down = false; } function onWheel(event) { event.preventDefault(); const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1; zoom *= zoomFactor; zoom = Math.max(0.1, Math.min(zoom, 100)); material.uniforms.u_zoom.value = zoom; } // Touch events for mobile support let touch = { x: 0, y: 0, down: false }; function onTouchStart(event) { event.preventDefault(); if (event.touches.length === 1) { touch.down = true; touch.x = event.touches[0].clientX; touch.y = event.touches[0].clientY; } } function onTouchMove(event) { event.preventDefault(); if (touch.down && event.touches.length === 1) { const deltaX = (event.touches[0].clientX - touch.x) / window.innerWidth * 4 / zoom; const deltaY = (event.touches[0].clientY - touch.y) / window.innerHeight * 4 / zoom; offset.x -= deltaX; offset.y += deltaY; material.uniforms.u_offset.value.set(offset.x, offset.y); touch.x = event.touches[0].clientX; touch.y = event.touches[0].clientY; } } function onTouchEnd(event) { event.preventDefault(); touch.down = false; } function onKeyDown(event) { if (event.code === 'Space') { event.preventDefault(); isPlaying = !isPlaying; } } function onWindowResize() { const width = window.innerWidth; const height = window.innerHeight; renderer.setSize(width, height); material.uniforms.u_resolution.value.set(width, height); } function animate() { requestAnimationFrame(animate); if (isPlaying) { const time = Date.now() * 0.001; material.uniforms.u_time.value = time; // Rotate the complex constant for morphing effect const angle = time * 0.3; // Moderate pace const radius = 0.7885; material.uniforms.u_c.value.set( radius * Math.cos(angle), radius * Math.sin(angle) ); } renderer.render(scene, camera); } // Initialize and start init(); animate(); </script> </body> </html> ``` This creates a full-screen Julia set fractal with the following features: 1. **High-iteration Julia set computation** - 256 iterations in the fragment shader for detailed fractals 2. **Smooth color transitions** - From golden yellow through orange, magenta, purple to deep indigo 3. **Morphing animation** - The complex constant rotates at a steady pace to create morphing effects 4. **Interactive controls**: - Click and drag to pan around the fractal - Mouse wheel to zoom in/out (pinch zoom on mobile) - Spacebar to toggle play/pause 5. **Mobile support** - Touch events for panning on mobile devices 6. **Responsive** - Automatically adjusts to window resizing The fractal uses the Julia set formula z = z² + c, where c rotates continuously to create the morphing effect. The color scheme uses smooth interpolation between the specified colors, and the shader is optimized for real-time rendering.