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