
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.