Spaces:
Running
Running
| /** | |
| * 🌿 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<uniform> 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, 3>( | |
| 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; | |