/** * 🌿 Ivy's GPU Art Studio * Tab 4: Generative Patterns * * Procedural pattern generation using fragment shaders * Perlin noise, Voronoi, waves, plasma, kaleidoscope, and more! */ class PatternsRenderer { constructor() { this.device = null; this.context = null; this.format = null; // Pattern parameters this.params = { type: 5, // 0-9 pattern types, default ivy (5) palette: 0, // 0-8 palettes scale: 1.0, speed: 1.0, complexity: 5, intensity: 1.0, animate: true, mouseReact: false }; this.input = null; this.animationLoop = null; this.isActive = false; this.time = 0; } async init(device, context, format, canvas) { this.device = device; this.context = context; this.format = format; this.canvas = canvas; // Create shader const shaderModule = device.createShaderModule({ label: "Patterns Shader", code: this.getShaderCode() }); // Create uniform buffer - increased size for new params this.uniformBuffer = device.createBuffer({ label: "Patterns 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: "Patterns 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); // Create animation loop this.animationLoop = new WebGPUUtils.AnimationLoop((dt, time) => { this.time += dt * this.params.speed; this.render(); }); } start() { this.isActive = true; this.animationLoop.start(); } stop() { this.isActive = false; this.animationLoop.stop(); } reset() { this.time = 0; } setType(type) { const types = { noise: 0, voronoi: 1, waves: 2, plasma: 3, kaleidoscope: 4, ivy: 5, hexagons: 6, spiral: 7, reaction: 8, circuits: 9 }; this.params.type = types[type] ?? 5; } setPalette(palette) { const palettes = { ivy: 0, rainbow: 1, fire: 2, ocean: 3, neon: 4, sunset: 5, cosmic: 6, candy: 7, monochrome: 8 }; this.params.palette = palettes[palette] ?? 0; } setScale(scale) { this.params.scale = scale; } setSpeed(speed) { this.params.speed = speed; } setComplexity(complexity) { this.params.complexity = complexity; } setIntensity(intensity) { this.params.intensity = intensity; } setAnimate(animate) { this.params.animate = animate; } setMouseReact(react) { this.params.mouseReact = react; } updateUniforms() { const aspect = this.canvas.width / this.canvas.height; const data = new Float32Array([ this.params.type, // 0 this.params.palette, // 4 this.params.scale, // 8 this.params.complexity, // 12 this.time, // 16 aspect, // 20 this.input.mouseX, // 24 this.input.mouseY, // 28 this.input.isPressed ? 1.0 : 0.0, // 32 this.params.intensity, // 36 this.params.animate ? 1.0 : 0.0, // 40 this.params.mouseReact ? 1.0 : 0.0, // 44 0.0, 0.0, 0.0, 0.0 // padding ]); 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); renderPass.end(); this.device.queue.submit([commandEncoder.finish()]); } getShaderCode() { return /* wgsl */ ` struct Uniforms { patternType: f32, palette: f32, scale: f32, complexity: f32, time: f32, aspect: f32, mouseX: f32, mouseY: f32, mousePressed: f32, intensity: f32, animate: f32, mouseReact: 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 { 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; } // ============================================ // Color Palettes // ============================================ fn getPaletteColor(t: f32, paletteId: i32) -> vec3f { let tt = fract(t); // Ivy Green if (paletteId == 0) { return vec3f(0.1 + 0.2 * tt, 0.4 + 0.5 * tt, 0.15 + 0.2 * tt); } // Rainbow else if (paletteId == 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)) ); } // Fire else if (paletteId == 2) { return vec3f(min(1.0, tt * 2.0), tt * tt, tt * tt * tt); } // Ocean else if (paletteId == 3) { return vec3f(0.0 + 0.2 * tt, 0.3 + 0.4 * tt, 0.5 + 0.5 * tt); } // Neon else if (paletteId == 4) { return vec3f( 0.5 + 0.5 * sin(tt * 6.28), 0.5 + 0.5 * sin(tt * 6.28 + 2.094), 0.5 + 0.5 * sin(tt * 6.28 + 4.188) ); } // Sunset else if (paletteId == 5) { return vec3f(0.9 - 0.3 * tt, 0.3 + 0.3 * tt, 0.4 + 0.4 * tt); } // Cosmic else if (paletteId == 6) { return vec3f( 0.1 + 0.4 * sin(tt * 6.28), 0.05 + 0.2 * sin(tt * 6.28 + 2.0), 0.3 + 0.6 * sin(tt * 6.28 + 4.0) ); } // Candy else if (paletteId == 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) ); } // Monochrome else { return vec3f(tt, tt, tt); } } // ============================================ // Noise Functions // ============================================ fn hash21(p: vec2f) -> f32 { var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); } fn hash22(p: vec2f) -> vec2f { let n = sin(dot(p, vec2f(41.0, 289.0))); return fract(vec2f(262144.0, 32768.0) * n) * 2.0 - 1.0; } fn noise(p: vec2f) -> f32 { let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f); return mix( mix(hash21(i + vec2f(0.0, 0.0)), hash21(i + vec2f(1.0, 0.0)), u.x), mix(hash21(i + vec2f(0.0, 1.0)), hash21(i + vec2f(1.0, 1.0)), u.x), u.y ); } fn fbm(p: vec2f, octaves: i32) -> f32 { var value = 0.0; var amplitude = 0.5; var frequency = 1.0; var pos = p; for (var i = 0; i < octaves; i++) { value += amplitude * noise(pos * frequency); amplitude *= 0.5; frequency *= 2.0; } return value; } // Voronoi fn voronoi(p: vec2f) -> vec3f { let n = floor(p); let f = fract(p); var minDist = 1.0; var minDist2 = 1.0; var cellId = vec2f(0.0); for (var j = -1; j <= 1; j++) { for (var i = -1; i <= 1; i++) { let g = vec2f(f32(i), f32(j)); let o = hash22(n + g) * 0.5 + 0.5; let r = g + o - f; let d = dot(r, r); if (d < minDist) { minDist2 = minDist; minDist = d; cellId = n + g; } else if (d < minDist2) { minDist2 = d; } } } return vec3f(sqrt(minDist), sqrt(minDist2) - sqrt(minDist), hash21(cellId)); } // ============================================ // Pattern Functions // ============================================ fn perlinPattern(uv: vec2f, t: f32) -> f32 { let p = uv * u.scale * 5.0; let animT = select(0.0, t, u.animate > 0.5); return fbm(p + vec2f(animT * 0.2, animT * 0.15), i32(u.complexity)); } fn voronoiPattern(uv: vec2f, t: f32) -> f32 { let animT = select(0.0, t, u.animate > 0.5); let p = uv * u.scale * 5.0 + vec2f(animT * 0.1, animT * 0.05); let v = voronoi(p); return v.z + v.y * 0.5; } fn wavesPattern(uv: vec2f, t: f32) -> f32 { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; p *= u.scale; var value = 0.0; let octaves = i32(u.complexity); let animT = select(0.0, t, u.animate > 0.5); for (var i = 0; i < octaves; i++) { let freq = f32(i + 1) * 3.0; let phase = animT * (0.5 + f32(i) * 0.1); value += sin(p.x * freq + phase) * cos(p.y * freq * 0.7 + phase * 0.8) / freq; } return value * 0.5 + 0.5; } fn plasmaPattern(uv: vec2f, t: f32) -> f32 { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; p *= u.scale * 2.0; let animT = select(0.0, t, u.animate > 0.5); var v = 0.0; v += sin(p.x * 10.0 + animT); v += sin(10.0 * (p.x * sin(animT / 2.0) + p.y * cos(animT / 3.0)) + animT); v += sin(sqrt(100.0 * (p.x * p.x + p.y * p.y) + 1.0) + animT); let cx = p.x + 0.5 * sin(animT / 5.0); let cy = p.y + 0.5 * cos(animT / 3.0); v += sin(sqrt(100.0 * (cx * cx + cy * cy) + 1.0) + animT); return (v / 4.0) * 0.5 + 0.5; } fn kaleidoscopePattern(uv: vec2f, t: f32) -> f32 { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; var r = length(p); var a = atan2(p.y, p.x); let segments = f32(i32(u.complexity) + 3); a = abs(((a / 3.14159 * 0.5 + 0.5) * segments) % 2.0 - 1.0) * 3.14159; p = vec2f(cos(a), sin(a)) * r; p *= u.scale * 2.0; let animT = select(0.0, t, u.animate > 0.5); p += vec2f(animT * 0.3, animT * 0.2); let n = fbm(p, 4); let fade = 1.0 - smoothstep(0.5, 1.0, r); return n * fade + r * 0.3; } fn ivyPattern(uv: vec2f, t: f32) -> f32 { var p = (uv - 0.5) * 2.0 * u.scale; p.x *= u.aspect; var value = 0.0; let animT = select(0.0, t, u.animate > 0.5); for (var vine = 0; vine < 5; vine++) { let vf = f32(vine); let vineOffset = vec2f(sin(vf * 1.5 + animT * 0.2) * 0.3, vf * 0.4 - 1.0); var vp = p - vineOffset; let curve = sin(vp.y * 3.0 + animT * 0.5 + vf) * 0.2; vp.x -= curve; let stemDist = abs(vp.x); let stemGlow = exp(-stemDist * 30.0); value += stemGlow * 0.3; for (var leaf = 0; leaf < 6; leaf++) { let lf = f32(leaf); let leafY = vf * 0.4 - 1.0 + lf * 0.3; let side = select(-1.0, 1.0, leaf % 2 == 0); let leafCenter = vec2f( vineOffset.x + sin(leafY * 3.0 + animT * 0.5 + vf) * 0.2 + side * 0.15, leafY ); var lp = p - leafCenter; let leafDist = length(lp * vec2f(1.0, 1.5)) - 0.06; let leafGlow = exp(-max(0.0, leafDist) * 40.0); value += leafGlow * 0.5; } } return clamp(value, 0.0, 1.0); } fn hexagonsPattern(uv: vec2f, t: f32) -> f32 { var p = (uv - 0.5) * u.scale * 10.0; p.x *= u.aspect; let animT = select(0.0, t, u.animate > 0.5); // Hexagonal grid let s = vec2f(1.0, 1.732); let a = (p / s) % 2.0 - 1.0; let b = ((p + s * 0.5) / s) % 2.0 - 1.0; let gv = select(a, b, dot(a, a) > dot(b, b)); let hexDist = max(abs(gv.x), abs(gv.y * 0.866 + gv.x * 0.5)); let pulse = sin(animT * 2.0 + length(p) * 0.5) * 0.5 + 0.5; return (1.0 - smoothstep(0.4, 0.5, hexDist)) * pulse; } fn spiralPattern(uv: vec2f, t: f32) -> f32 { var p = (uv - 0.5) * 2.0; p.x *= u.aspect; let r = length(p); let a = atan2(p.y, p.x); let animT = select(0.0, t, u.animate > 0.5); let spiral = sin(a * u.complexity + r * 10.0 * u.scale - animT * 3.0); let rings = sin(r * 20.0 - animT * 2.0); return (spiral * 0.5 + 0.5) * (1.0 - r * 0.5); } fn reactionPattern(uv: vec2f, t: f32) -> f32 { var p = uv * u.scale * 8.0; let animT = select(0.0, t, u.animate > 0.5); var a = fbm(p + vec2f(animT * 0.1, 0.0), i32(u.complexity)); var b = fbm(p + vec2f(0.0, animT * 0.1) + a * 2.0, i32(u.complexity)); var c = fbm(p + b * 2.0 + vec2f(animT * 0.05), i32(u.complexity)); return c; } fn circuitsPattern(uv: vec2f, t: f32) -> f32 { var p = uv * u.scale * 5.0; let animT = select(0.0, t, u.animate > 0.5); let grid = floor(p); let f = fract(p); let randVal = hash21(grid); let lineX = step(0.45, f.x) * step(f.x, 0.55); let lineY = step(0.45, f.y) * step(f.y, 0.55); var circuit = 0.0; if (randVal > 0.5) { circuit = lineX; } else { circuit = lineY; } // Nodes at intersections let nodeDist = length(f - 0.5); let node = 1.0 - smoothstep(0.1, 0.15, nodeDist); // Pulse animation let pulse = sin(animT * 3.0 + hash21(grid) * 6.28) * 0.5 + 0.5; return (circuit + node) * (0.5 + pulse * 0.5); } @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { let patternType = i32(u.patternType); let paletteId = i32(u.palette); var t = u.time; var value: f32 = 0.0; if (patternType == 0) { value = perlinPattern(input.uv, t); } else if (patternType == 1) { value = voronoiPattern(input.uv, t); } else if (patternType == 2) { value = wavesPattern(input.uv, t); } else if (patternType == 3) { value = plasmaPattern(input.uv, t); } else if (patternType == 4) { value = kaleidoscopePattern(input.uv, t); } else if (patternType == 5) { value = ivyPattern(input.uv, t); } else if (patternType == 6) { value = hexagonsPattern(input.uv, t); } else if (patternType == 7) { value = spiralPattern(input.uv, t); } else if (patternType == 8) { value = reactionPattern(input.uv, t); } else { value = circuitsPattern(input.uv, t); } // Apply intensity value *= u.intensity; // Get color from palette var color = getPaletteColor(value, paletteId); // Mouse interaction if (u.mouseReact > 0.5 || u.mousePressed > 0.5) { let mouse = vec2f(u.mouseX, u.mouseY); let dist = distance(input.uv, mouse); let glow = exp(-dist * 8.0) * 0.6; color += vec3f(glow, glow * 0.7, glow * 0.9); } return vec4f(color, 1.0); } `; } } // Export window.PatternsRenderer = PatternsRenderer;