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