/** * 🌿 Ivy's GPU Art Studio * Tab 1: Fractals — Mandelbrot, Julia, Burning Ship, and more! * * Interactive fractal explorer with zoom, pan, and color palettes */ class FractalsRenderer { constructor() { this.device = null; this.context = null; this.format = null; this.pipeline = null; this.uniformBuffer = null; this.bindGroup = null; // Fractal parameters this.params = { type: 0, // 0=Mandelbrot, 1=Julia, 2=BurningShip, 3=Ivy, 4=Tricorn, 5=Phoenix, 6=Newton, 7=Celtic iterations: 100, palette: 0, juliaReal: -0.7, juliaImag: 0.27, centerX: -0.5, centerY: 0.0, zoom: 1.0, time: 0.0, power: 2.0, colorShift: 0.0, animate: false, smoothing: true }; this.input = null; this.animationLoop = null; this.isActive = false; // Drag state this.isDragging = false; this.dragStartX = 0; this.dragStartY = 0; this.dragStartCenterX = 0; this.dragStartCenterY = 0; } async init(device, context, format, canvas) { this.device = device; this.context = context; this.format = format; this.canvas = canvas; // Create shader const shaderCode = this.getShaderCode(); const shaderModule = device.createShaderModule({ label: "Fractals Shader", code: shaderCode }); // Create uniform buffer - increased size for new params // 16 floats * 4 bytes = 64 bytes, padded to 80 for alignment this.uniformBuffer = device.createBuffer({ label: "Fractals Uniforms", size: 80, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); // Create bind group layout const bindGroupLayout = device.createBindGroupLayout({ entries: [ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } } ] }); // Create pipeline this.pipeline = device.createRenderPipeline({ label: "Fractals Pipeline", layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }), vertex: { module: shaderModule, entryPoint: "vertexMain" }, fragment: { module: shaderModule, entryPoint: "fragmentMain", targets: [{ format }] }, primitive: { topology: "triangle-list" } }); // Create bind group this.bindGroup = device.createBindGroup({ layout: bindGroupLayout, entries: [ { binding: 0, resource: { buffer: this.uniformBuffer } } ] }); // Setup input this.input = new WebGPUUtils.InputHandler(canvas); this.setupDragEvents(); // Create animation loop this.animationLoop = new WebGPUUtils.AnimationLoop((dt, time) => { this.params.time = time; this.render(); }); } setupDragEvents() { this.canvas.addEventListener("mousedown", e => { this.isDragging = true; const rect = this.canvas.getBoundingClientRect(); this.dragStartX = (e.clientX - rect.left) / rect.width; this.dragStartY = (e.clientY - rect.top) / rect.height; this.dragStartCenterX = this.params.centerX; this.dragStartCenterY = this.params.centerY; }); this.canvas.addEventListener("mousemove", e => { if (!this.isDragging) return; const rect = this.canvas.getBoundingClientRect(); const currentX = (e.clientX - rect.left) / rect.width; const currentY = (e.clientY - rect.top) / rect.height; const dx = ((currentX - this.dragStartX) * 4.0) / this.params.zoom; const dy = ((currentY - this.dragStartY) * 4.0) / this.params.zoom; this.params.centerX = this.dragStartCenterX - dx; this.params.centerY = this.dragStartCenterY + dy; // Flip Y }); this.canvas.addEventListener("mouseup", () => { this.isDragging = false; }); this.canvas.addEventListener("mouseleave", () => { this.isDragging = false; }); // Zoom with wheel this.canvas.addEventListener( "wheel", e => { e.preventDefault(); const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; this.params.zoom *= zoomFactor; this.params.zoom = Math.max(0.1, Math.min(this.params.zoom, 1000000)); }, { passive: false } ); } start() { this.isActive = true; this.animationLoop.start(); } stop() { this.isActive = false; this.animationLoop.stop(); } reset() { this.params.centerX = -0.5; this.params.centerY = 0.0; this.params.zoom = 1.0; } setType(type) { const types = { mandelbrot: 0, julia: 1, "burning-ship": 2, ivy: 3, tricorn: 4, phoenix: 5, newton: 6, celtic: 7 }; this.params.type = types[type] ?? 0; } setPalette(palette) { const palettes = { ivy: 0, rainbow: 1, fire: 2, ocean: 3, neon: 4, sunset: 5, cosmic: 6, candy: 7, matrix: 8, grayscale: 9 }; this.params.palette = palettes[palette] ?? 1; } setIterations(iterations) { this.params.iterations = iterations; } setJuliaParams(real, imag) { this.params.juliaReal = real; this.params.juliaImag = imag; } setPower(power) { this.params.power = power; } setColorShift(shift) { this.params.colorShift = shift; } setAnimate(animate) { this.params.animate = animate; } setSmoothColoring(smoothing) { this.params.smoothing = smoothing; } updateUniforms() { const aspect = this.canvas.width / this.canvas.height; const data = new Float32Array([ this.params.centerX, // offset 0 this.params.centerY, // offset 4 this.params.zoom, // offset 8 aspect, // offset 12 this.params.iterations, // offset 16 this.params.type, // offset 20 this.params.palette, // offset 24 this.params.time, // offset 28 this.params.juliaReal, // offset 32 this.params.juliaImag, // offset 36 this.params.power, // offset 40 this.params.colorShift, // offset 44 this.params.animate ? 1.0 : 0.0, // offset 48 this.params.smoothing ? 1.0 : 0.0, // offset 52 0.0, // padding 56 0.0 // padding 60 ]); this.device.queue.writeBuffer(this.uniformBuffer, 0, data); } render() { if (!this.isActive) return; WebGPUUtils.resizeCanvasToDisplaySize(this.canvas, window.devicePixelRatio); this.updateUniforms(); const commandEncoder = this.device.createCommandEncoder(); const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [ { view: this.context.getCurrentTexture().createView(), clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" } ] }); renderPass.setPipeline(this.pipeline); renderPass.setBindGroup(0, this.bindGroup); renderPass.draw(3); // Full-screen triangle renderPass.end(); this.device.queue.submit([commandEncoder.finish()]); } getShaderCode() { return /* wgsl */ ` struct Uniforms { centerX: f32, centerY: f32, zoom: f32, aspect: f32, iterations: f32, fractalType: f32, palette: f32, time: f32, juliaReal: f32, juliaImag: f32, power: f32, colorShift: f32, animate: f32, smoothColoring: f32, } @group(0) @binding(0) var u: Uniforms; struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f, } @vertex fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { // Full-screen triangle var pos = array( vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0) ); var output: VertexOutput; output.position = vec4f(pos[vertexIndex], 0.0, 1.0); output.uv = pos[vertexIndex] * 0.5 + 0.5; return output; } // Complex number operations fn cmul(a: vec2f, b: vec2f) -> vec2f { return vec2f(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); } fn cabs2(z: vec2f) -> f32 { return z.x * z.x + z.y * z.y; } // Mandelbrot iteration fn mandelbrot(c: vec2f, maxIter: i32) -> f32 { var z = vec2f(0.0, 0.0); var i: i32 = 0; for (; i < maxIter; i++) { if (cabs2(z) > 4.0) { break; } z = cmul(z, z) + c; } if (i >= maxIter) { return 0.0; } // Smooth coloring let log_zn = log(cabs2(z)) / 2.0; let nu = log(log_zn / log(2.0)) / log(2.0); return (f32(i) + 1.0 - nu) / f32(maxIter); } // Julia iteration fn julia(z0: vec2f, c: vec2f, maxIter: i32) -> f32 { var z = z0; var i: i32 = 0; for (; i < maxIter; i++) { if (cabs2(z) > 4.0) { break; } z = cmul(z, z) + c; } if (i >= maxIter) { return 0.0; } let log_zn = log(cabs2(z)) / 2.0; let nu = log(log_zn / log(2.0)) / log(2.0); return (f32(i) + 1.0 - nu) / f32(maxIter); } // Burning Ship iteration fn burningShip(c: vec2f, maxIter: i32) -> f32 { var z = vec2f(0.0, 0.0); var i: i32 = 0; for (; i < maxIter; i++) { if (cabs2(z) > 4.0) { break; } z = vec2f(abs(z.x), abs(z.y)); z = cmul(z, z) + c; } if (i >= maxIter) { return 0.0; } let log_zn = log(cabs2(z)) / 2.0; let nu = log(log_zn / log(2.0)) / log(2.0); return (f32(i) + 1.0 - nu) / f32(maxIter); } // 🌿 Ivy Fractal - A beautiful organic plant-like fractal! fn ivyFractal(c: vec2f, maxIter: i32, time: f32) -> f32 { var z = c; var i: i32 = 0; // Newton fractal for z^3 - 1 = 0 (gives 3 roots like a leaf pattern) // Combined with organic perturbation let root1 = vec2f(1.0, 0.0); let root2 = vec2f(-0.5, 0.866); let root3 = vec2f(-0.5, -0.866); let tolerance = 0.0001; var minDist: f32 = 100.0; var whichRoot: i32 = 0; for (; i < maxIter; i++) { // z^3 let z2 = cmul(z, z); let z3 = cmul(z2, z); // z^3 - 1 let f = z3 - vec2f(1.0, 0.0); // 3z^2 (derivative) let df = 3.0 * z2; // Avoid division by zero let dfMag = cabs2(df); if (dfMag < 0.0001) { break; } // Newton step: z = z - f(z)/f'(z) // Division: (a+bi)/(c+di) = (ac+bd)/(c²+d²) + i(bc-ad)/(c²+d²) let denom = dfMag; let newZ = z - vec2f( (f.x * df.x + f.y * df.y) / denom, (f.y * df.x - f.x * df.y) / denom ); // Add organic "growth" perturbation (makes it look like ivy!) let growth = sin(f32(i) * 0.5 + time * 0.5) * 0.02; z = newZ + vec2f(growth * z.y, -growth * z.x); // Check distances to roots let d1 = cabs2(z - root1); let d2 = cabs2(z - root2); let d3 = cabs2(z - root3); if (d1 < minDist) { minDist = d1; whichRoot = 0; } if (d2 < minDist) { minDist = d2; whichRoot = 1; } if (d3 < minDist) { minDist = d3; whichRoot = 2; } if (minDist < tolerance) { break; } } // Color based on which root and how many iterations let baseHue = f32(whichRoot) / 3.0; let iterFactor = f32(i) / f32(maxIter); return baseHue + iterFactor * 0.3; } // Color palettes - 10 options! fn palette(t: f32, paletteType: i32, colorShift: f32, animTime: f32, doAnimate: bool) -> vec3f { if (t <= 0.0) { return vec3f(0.0, 0.0, 0.0); } var tt = fract(t * 5.0 + colorShift); if (doAnimate) { tt = fract(tt + animTime * 0.1); } // 0: Ivy Green 🌿 if (paletteType == 0) { return vec3f( 0.1 + 0.2 * tt, 0.4 + 0.5 * tt, 0.2 + 0.2 * tt ); } // 1: Rainbow 🌈 else if (paletteType == 1) { return vec3f( 0.5 + 0.5 * cos(6.28318 * (tt + 0.0)), 0.5 + 0.5 * cos(6.28318 * (tt + 0.33)), 0.5 + 0.5 * cos(6.28318 * (tt + 0.67)) ); } // 2: Fire šŸ”„ else if (paletteType == 2) { return vec3f( min(1.0, tt * 3.0), max(0.0, min(1.0, tt * 3.0 - 1.0)), max(0.0, min(1.0, tt * 3.0 - 2.0)) ); } // 3: Ocean 🌊 else if (paletteType == 3) { return vec3f( 0.0 + 0.2 * tt, 0.3 + 0.4 * tt, 0.5 + 0.5 * tt ); } // 4: Neon šŸ’” else if (paletteType == 4) { return vec3f( 0.5 + 0.5 * sin(tt * 6.28318), 0.5 + 0.5 * sin(tt * 6.28318 + 2.094), 0.5 + 0.5 * sin(tt * 6.28318 + 4.188) ); } // 5: Sunset šŸŒ… else if (paletteType == 5) { return vec3f( 0.9 - 0.4 * tt, 0.3 + 0.3 * tt, 0.4 + 0.4 * tt ); } // 6: Cosmic 🌌 else if (paletteType == 6) { return vec3f( 0.1 + 0.4 * sin(tt * 6.28 + 0.0), 0.05 + 0.2 * sin(tt * 6.28 + 2.0), 0.3 + 0.6 * sin(tt * 6.28 + 4.0) ); } // 7: Candy šŸ¬ else if (paletteType == 7) { return vec3f( 0.8 + 0.2 * sin(tt * 12.56), 0.4 + 0.4 * sin(tt * 12.56 + 2.0), 0.7 + 0.3 * sin(tt * 12.56 + 4.0) ); } // 8: Matrix šŸ’š else if (paletteType == 8) { return vec3f( 0.0, 0.2 + 0.8 * tt, 0.0 ); } // 9: Grayscale ⚫ else { return vec3f(tt, tt, tt); } } // ===== FRACTAL FUNCTIONS ===== // Tricorn (Mandelbar) - conjugate Mandelbrot fn tricorn(c: vec2f, maxIter: i32, power: f32) -> f32 { var z = vec2f(0.0, 0.0); var i: i32 = 0; for (; i < maxIter; i++) { if (cabs2(z) > 4.0) { break; } // Conjugate: (x, -y) z = vec2f(z.x, -z.y); z = cmul(z, z) + c; } if (i >= maxIter) { return 0.0; } let log_zn = log(cabs2(z)) / 2.0; let nu = log(log_zn / log(2.0)) / log(2.0); return (f32(i) + 1.0 - nu) / f32(maxIter); } // Phoenix fractal fn phoenix(z0: vec2f, c: vec2f, p: vec2f, maxIter: i32) -> f32 { var z = z0; var zPrev = vec2f(0.0, 0.0); var i: i32 = 0; for (; i < maxIter; i++) { if (cabs2(z) > 4.0) { break; } let zNew = cmul(z, z) + c + cmul(p, zPrev); zPrev = z; z = zNew; } if (i >= maxIter) { return 0.0; } let log_zn = log(cabs2(z)) / 2.0; let nu = log(log_zn / log(2.0)) / log(2.0); return (f32(i) + 1.0 - nu) / f32(maxIter); } // Newton fractal (z^3 - 1) fn newton(c: vec2f, maxIter: i32) -> f32 { var z = c; var i: i32 = 0; let root1 = vec2f(1.0, 0.0); let root2 = vec2f(-0.5, 0.866025); let root3 = vec2f(-0.5, -0.866025); let tolerance = 0.0001; for (; i < maxIter; i++) { let z2 = cmul(z, z); let z3 = cmul(z2, z); let f = z3 - vec2f(1.0, 0.0); let df = 3.0 * z2; let dfMag = cabs2(df); if (dfMag < 0.0001) { break; } z = z - vec2f( (f.x * df.x + f.y * df.y) / dfMag, (f.y * df.x - f.x * df.y) / dfMag ); let d1 = cabs2(z - root1); let d2 = cabs2(z - root2); let d3 = cabs2(z - root3); if (d1 < tolerance || d2 < tolerance || d3 < tolerance) { break; } } return f32(i) / f32(maxIter); } // Celtic Mandelbrot variant fn celtic(c: vec2f, maxIter: i32) -> f32 { var z = vec2f(0.0, 0.0); var i: i32 = 0; for (; i < maxIter; i++) { if (cabs2(z) > 4.0) { break; } // Celtic: |Re(z²)| + i*Im(z²) + c let z2 = cmul(z, z); z = vec2f(abs(z2.x), z2.y) + c; } if (i >= maxIter) { return 0.0; } let log_zn = log(cabs2(z)) / 2.0; let nu = log(log_zn / log(2.0)) / log(2.0); return (f32(i) + 1.0 - nu) / f32(maxIter); } @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { // Map UV to complex plane var uv = input.uv * 2.0 - 1.0; uv.x *= u.aspect; // Apply zoom and pan let c = vec2f( uv.x / u.zoom + u.centerX, uv.y / u.zoom + u.centerY ); let maxIter = i32(u.iterations); var t: f32 = 0.0; // Select fractal type let fractalType = i32(u.fractalType); if (fractalType == 0) { // Mandelbrot t = mandelbrot(c, maxIter); } else if (fractalType == 1) { // Julia let juliaC = vec2f(u.juliaReal, u.juliaImag); t = julia(c, juliaC, maxIter); } else if (fractalType == 2) { // Burning Ship t = burningShip(c, maxIter); } else if (fractalType == 3) { // 🌿 Ivy Fractal t = ivyFractal(c, maxIter, u.time); } else if (fractalType == 4) { // Tricorn t = tricorn(c, maxIter, u.power); } else if (fractalType == 5) { // Phoenix let juliaC = vec2f(u.juliaReal, u.juliaImag); let phoenixP = vec2f(-0.5, 0.0); t = phoenix(c, juliaC, phoenixP, maxIter); } else if (fractalType == 6) { // Newton t = newton(c, maxIter); } else if (fractalType == 7) { // Celtic t = celtic(c, maxIter); } // Apply palette let doAnimate = u.animate > 0.5; var color = palette(t, i32(u.palette), u.colorShift, u.time, doAnimate); // Special tint for Ivy fractal if (fractalType == 3) { color = mix(color, vec3f(0.13, 0.77, 0.37), 0.3); } return vec4f(color, 1.0); } `; } } // Export window.FractalsRenderer = FractalsRenderer;