ivy-gpu-art-studio / js /fractals.js
Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 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;