oneshotlm
Compare all models on this prompt →

Reaction-diffusion · gemma-4-26b-a4b-it

Run a Gray-Scott reaction-diffusion simulation on a canvas that grows organic Turing patterns in real time, with the mouse seeding new growth.

⚠ no runnable output

This one-shot run produced no runnable artifact.

agent produced no index.html

The transcript below shows what the model did — read it to see why the one shot fell short.

transcript · 2 messages
user

Run a Gray-Scott reaction-diffusion simulation on a canvas that grows organic Turing patterns in real time, with the mouse seeding new growth.

assistant
thinking
The user wants a single-page web app (`index.html`) containing a Gray-Scott reaction-diffusion simulation.
The simulation should:
1.  Run on a canvas.
2.  Grow organic Turing patterns in real time.
3.  Allow the user to seed new growth with the mouse.
4.  Be a single file (CSS in `<style>`, JS in `<script>`).
5.  Use NO external resources (no CDNs, no network requests).

Plan:
1.  Implement the Gray-Scott model using WebGL for performance (it's computationally expensive for CPU).
2.  Use two textures for ping-ponging between simulation steps.
3.  Use a fragment shader for the reaction-diffusion calculation.
4.  Use another fragment shader for rendering the patterns (mapping concentration to colors).
5.  Implement mouse interaction to add "feed" or "kill" or just substance $U/V$ to the grid.
6.  Include a UI to tweak parameters like $f$ (feed rate) and $k$ (kill rate).

Gray-Scott equations:
$\frac{\partial u}{\partial t} = D_u \nabla^2 u - uv^2 + f(1-u)$
$\frac{\partial v}{\partial t} = D_v \nabla^2 v + uv^2 - (f+k)v$

Commonly used parameters for patterns:
- $D_u \approx 1.0$, $D_v \approx 0.5$
- $f \approx 0.03, k \approx 0.06$ (mitosis/spots)
- $f \approx 0.055, k \approx 0.062$ (coral)
- $f \approx 0.03, k \approx 0.06$ (labyrinth)

I'll use WebGL2 if possible, or WebGL1. WebGL2 is widely supported now.

Structure of `index.html`:
- `<!DOCTYPE html>`
- `<style>` for layout and UI.
- `<body>` with `<canvas>` and a UI overlay.
- `<script>` for WebGL logic.

WebGL Implementation Details:
- `Simulation Shader`: Takes $U$ and $V$ textures, calculates next state.
- `Render Shader`: Takes $V$ (or $U$ and $V$) texture and maps to color.
- `Ping-ponging`: Two framebuffers, switching between them every frame.
- `Interactivity`: Pass mouse position and radius to the simulation shader via a uniform.

Let's start writing the code.

```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Gray-Scott Reaction-Diffusion</title> <style> body { margin: 0; overflow: hidden; background-color: #111; color: #eee; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } canvas { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; } #ui { position: absolute; top: 20px; left: 20px; background: rgba(0, 0, 0, 0.7); padding: 20px; border-radius: 8px; backdrop-filter: blur(10px); user-select: none; max-width: 280px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); } h1 { margin: 0 0 15px 0; font-size: 1.2rem; font-weight: 500; color: #0af; } .control-group { margin-bottom: 15px; } label { display: block; font-size: 0.85rem; margin-bottom: 5px; } input[type="range"] { width: 100%; cursor: pointer; } .value-display { float: right; font-family: monospace; color: #0af; } .buttons { display: flex; gap: 10px; margin-top: 10px; } button { flex: 1; background: #333; border: 1px solid #555; color: white; padding: 8px; border-radius: 4px; cursor: pointer; font-size: 0.8rem; } button:hover { background: #444; } .instructions { margin-top: 15px; font-size: 0.75rem; color: #888; line-height: 1.4; } </style> </head> <body> <canvas id="glCanvas"></canvas> <div id="ui"> <h1>Gray-Scott Diffusion</h1> <div class="control-group"> <label>Feed Rate (f) <span class="value-display" id="val-f">0.055</span></label> <input type="range" id="input-f" min="0.01" max="0.1" step="0.001" value="0.055"> </div> <div class="control-group"> <label>Kill Rate (k) <span class="value-display" id="val-k">0.062</span></label> <input type="range" id="input-k" min="0.01" max="0.07" step="0.001" value="0.062"> </div> <div class="control-group"> <label>Diffusion U <span class="value-display" id="val-du">1.0</span></label> <input type="range" id="input-du" min="0.1" max="1.0" step="0.01" value="1.0"> </div> <div class="control-group"> <label>Diffusion V <span class="value-display" id="val-dv">0.5</span></label> <input type="range" id="input-dv" min="0.1" max="1.0" step="0.01" value="0.5"> </div> <div class="buttons"> <button id="btn-reset">Reset</button> <button id="btn-clear">Clear</button> </div> <div class="instructions"> Click and drag to seed substance V.<br> Patterns vary wildly with f and k. </div> </div> <script> /** * GRAY-SCOTT REACTION-DIFFUSION SIMULATION * * Equations: * du/dt = Du * nabla^2(u) - uv^2 + f(1-u) * dv/dt = Dv * nabla^2(v) + uv^2 - (f+k)v */ const canvas = document.getElementById('glCanvas'); const gl = canvas.getContext('webgl2'); if (!gl) { alert("WebGL 2 not supported. This simulation requires WebGL 2."); } // Simulation parameters const params = { f: 0.055, k: 0.062, du: 1.0, dv: 0.5, mousePress: false, mouseX: 0, mouseY: 0, mouseRadius: 0.02, mouseStrength: 0.8 }; // UI elements const controls = { f: document.getElementById('input-f'), k: document.getElementById('input-k'), du: document.getElementById('input-du'), dv: document.getElementById('input-dv'), valF: document.getElementById('val-f'), valK: document.getElementById('val-k'), valDu: document.getElementById('val-du'), valDv: document.getElementById('val-dv'), btnReset: document.getElementById('btn-reset'), btnClear: document.getElementById('btn-clear') }; // Update params from UI const updateUI = () => { params.f = parseFloat(controls.f.value); params.k = parseFloat(controls.k.value); params.du = parseFloat(controls.du.value); params.dv = parseFloat(controls.dv.value); controls.valF.innerText = params.f.toFixed(3); controls.valK.innerText = params.k.toFixed(3); controls.valDu.innerText = params.du.toFixed(2); controls.valDv.innerText = params.dv.toFixed(2); }; controls.f.addEventListener('input', updateUI); controls.k.addEventListener('input', updateUI); controls.du.addEventListener('input', updateUI); controls.dv.addEventListener('input', updateUI); controls.btnReset.addEventListener('click', resetSimulation); controls.btnClear.addEventListener('click', clearSimulation); // Mouse events canvas.addEventListener('mousedown', (e) => { params.mousePress = true; updateMouse(e); }); window.addEventListener('mouseup', () => { params.mousePress = false; }); canvas.addEventListener('mousemove', (e) => { updateMouse(e); }); function updateMouse(e) { params.mouseX = e.clientX / window.innerWidth; params.mouseY = 1.0 - (e.clientY / window.innerHeight); } // Shaders const simVertexShaderSource = `#version 300 es in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; const simFragmentShaderSource = `#version 300 es precision highp float; uniform sampler2D u_state; uniform vec2 u_resolution; uniform float u_f; uniform float u_k; uniform float u_du; uniform float u_dv; uniform vec2 u_mouse; uniform float u_mousePress; uniform float u_mouseRadius; out vec4 outColor; void main() { vec2 uv = gl_FragCoord.xy / u_resolution; vec2 texel = 1.0 / u_resolution; // Laplacian using 3x3 stencil vec2 state = texture(u_state, uv).rg; vec2 lap = vec2(0.0); // Center lap += state * -4.0; // Neighbors lap += texture(u_state, uv + vec2(texel.x, 0.0)).rg * 1.0; lap += texture(u_state, uv + vec2(-texel.x, 0.0)).rg * 1.0; lap += texture(u_state, uv + vec2(0.0, texel.y)).rg * 1.0; lap += texture(u_state, uv + vec2(0.0, -texel.y)).rg * 1.0; // Diagonals lap += texture(u_state, uv + vec2(texel.x, texel.y)).rg * 0.25; lap += texture(u_state, uv + vec2(-texel.x, texel.y)).rg * 0.25; lap += texture(u_state, uv + vec2(texel.x, -texel.y)).rg * 0.25; lap += texture(u_state, uv + vec2(-texel.x, -texel.y)).rg * 0.25; // Re-scale laplacian for diffusion? // Actually, standard discretisation for Laplacian is // center * -4 + sum(neighbors) // Or more accurately: center * -4/h^2 etc. // Since we use texel size, let's use a standard weight. // For simplicity, we'll use: // lap = (N + S + E + W - 4*C) / h^2 // In JS we'll just treat the laplacian as the delta. // The coefficients above are for a simplified kernel. // For Gray-Scott, the coefficients are usually: // [0, 1, 0, 1, -4, 1, 0, 1, 0] / h^2 // and diagonals are often included. // Let's use the standard 3x3 discrete laplacian: // 0.05, 0.2, 0.05 // 0.2, -1.0, 0.2 // 0.05, 0.2, 0.05 lap = vec2(0.0); lap += texture(u_state, uv + vec2(texel.x, 0.0)).rg * 0.2; lap += texture(u_state, uv + vec2(-texel.x, 0.0)).rg * 0.2; lap += texture(u_state, uv + vec2(0.0, texel.y)).rg * 0.2; lap += texture(u_state, uv + vec2(0.0, -texel.y)).rg * 0.2; lap += texture(u_state, uv + vec2(texel.x, texel.y)).rg * 0.05; lap += texture(u_state, uv + vec2(-texel.x, texel.y)).rg * 0.05; lap += texture(u_state, uv + vec2(texel.x, -texel.y)).rg * 0.05; lap += texture(u_state, uv + vec2(-texel.x, -texel.y)).rg * 0.05; lap -= state; float u = state.r; float v = state.g; float reaction = u * v * v; float du = u_du * lap.r; // Simplified: using lap as the driving force // Correct Laplacian application: // du = Du * Laplacian(u) - u*v^2 + f(1-u) // dv = Dv * Laplacian(v) + u*v^2 - (f+k)v // We need a separate laplacian for u and v? // Actually we are storing (u,v) in the rg channels. // The Laplacian calculated above is for the whole vector. // But u and v have different diffusion rates. // So we need to apply du and dv separately. // Redo Laplacian calculation per channel logic // Instead of one 'lap' for both, let's compute 'lapU' and 'lapV' // but since we only have 1 texture, we can use the same stencil. // We just apply du and dv to the same spatial laplacian. // Wait, the stencil above is for the vector. // Let's use: // lapU = kernel * u // lapV = kernel * v // Actually, let's just do: // float Lu = Laplacian(u); // float Lv = Laplacian(v); // The current 'lap' is actually (Laplacian(u), Laplacian(v)) if we think about it, // because we are applying the same stencil to the rg components. float du_dt = u_du * lap.r - reaction + u_f * (1.0 - u); float dv_dt = u_dv * lap.g + reaction - (u_f + u_k) * v; float next_u = u + du_dt; float next_v = v + dv_dt; // Mouse interaction: add V float dist = distance(uv, u_mouse); if (u_mousePress > 0.5 && dist < u_mouseRadius) { next_v += u_mouseStrength * (1.0 - dist/u_mouseRadius); } outColor = vec4(clamp(next_u, 0.0, 1.0), clamp(next_v, 0.0, 1.0), 0.0, 1.0); } `; const renderFragmentShaderSource = `#version 300 es precision highp float; uniform sampler2D u_state; uniform vec2 u_resolution; out vec4 outColor; // Color palette vec3 color1 = vec3(0.05, 0.05, 0.1); // Background vec3 color2 = vec3(0.0, 0.7, 1.0); // Pattern vec3 color3 = vec3(1.0, 1.0, 1.0); // Highlight void main() { vec2 uv = gl_FragCoord.xy / u_resolution; vec2 state = texture(u_state, uv).rg; float v = state.g; float u = state.r; // Map v to a color // v is the concentration of substance B // When v is high, we want bright colors. vec3 col = mix(color1, color2, smoothstep(0.0, 0.5, v)); col = mix(col, color3, smoothstep(0.4, 0.8, v)); outColor = vec4(col, 1.0); } `; // Helper to create shader function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } // Helper to create program function createProgram(gl, vsSource, fsSource) { const vs = createShader(gl, gl.VERTEX_SHADER, vsSource); const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource); const program = gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)); return null; } return program; } let simProgram, renderProgram; let quadBuffer; let textures = []; let framebuffers = []; let currentTextureIdx = 0; let simWidth, simHeight; function initWebGL() { simProgram = createProgram(gl, simVertexShaderSource, simFragmentShaderSource); renderProgram = createProgram(gl, simVertexShaderSource, renderFragmentShaderSource); quadBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1, ]), gl.STATIC_DRAW); resize(); } function resize() { // We use a fixed resolution for simulation for stability and speed, // then scale it up to the canvas size. // Or just use the canvas size. Let's use the canvas size but maybe slightly lower res for perf. simWidth = Math.floor(window.innerWidth / 1); // 1:1 scale simHeight = Math.floor(window.innerHeight / 1); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Cleanup old textures/FBOs textures.forEach(t => gl.deleteTexture(t)); framebuffers.forEach(f => gl.deleteFramebuffer(f)); textures = []; framebuffers = []; // Create two textures for ping-ponging for (let i = 0; i < 2; i++) { const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, simWidth, simHeight, 0, gl.RG, gl.FLOAT, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); textures.push(tex); const fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, fb); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); framebuffers.push(fb); } resetSimulation(); } function resetSimulation() { // Initialize texture with U=1, V=0 const data = new Float32Array(simWidth * simHeight * 2); for (let i = 0; i < data.length; i += 2) { data[i] = 1.0; // U data[i+1] = 0.0; // V } // Seed with some random V for (let i = 0; i < 50; i++) { const x = Math.floor(Math.random() * simWidth); const y = Math.floor(Math.random() * simHeight); const radius = 10; for (let dy = -radius; dy < radius; dy++) { for (let dx = -radius; dx < radius; dx++) { const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < simWidth && ny >= 0 && ny < simHeight) { const idx = (ny * simWidth + nx) * 2; if (Math.sqrt(dx*dx + dy*dy) < radius) { data[idx+1] = 1.0; } } } } } gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, simWidth, simHeight, gl.RG, gl.FLOAT, data); gl.bindTexture(gl.TEXTURE_2D, textures[1]); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, simWidth, simHeight, gl.RG, gl.FLOAT, data); } function clearSimulation() { const data = new Float32Array(simWidth * simHeight * 2); for (let i = 0; i < data.length; i += 2) { data[i] = 1.0; data[i+1] = 0.0; } gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, simWidth, simHeight, gl.RG, gl.FLOAT, data); gl.bindTexture(gl.TEXTURE_2D, textures[1]); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, simWidth, simHeight, gl.RG, gl.FLOAT, data); } function step() { // Perform multiple simulation steps per frame for speed const iterations = 8; gl.useProgram(simProgram); // Set common uniforms const uResLoc = gl.getUniformLocation(simProgram, "u_resolution"); const uFLoc = gl.getUniformLocation(simProgram, "u_f"); const uKLoc = gl.getUniformLocation(simProgram, "u_k"); const uDuLoc = gl.getUniformLocation(simProgram, "u_du"); const uDvLoc = gl.getUniformLocation(simProgram, "u_dv"); const uMouseLoc = gl.getUniformLocation(simProgram, "u_mouse"); const uMousePressLoc = gl.getUniformLocation(simProgram, "u_mousePress"); const uMouseRadiusLoc = gl.getUniformLocation(simProgram, "u_mouseRadius"); gl.uniform2f(uResLoc, simWidth, simHeight); gl.uniform1f(uFLoc, params.f); gl.uniform1f(uKLoc, params.k); gl.uniform1f(uDuLoc, params.du); gl.uniform1f(uDvLoc, params.dv); gl.uniform2f(uMouseLoc, params.mouseX, params.mouseY); gl.uniform1f(uMousePressLoc, params.mousePress ? 1.0 : 0.0); gl.uniform1f(uMouseRadiusLoc, params.mouseRadius); const posLoc = gl.getAttribLocation(simProgram, "position"); gl.enableVertexAttribArray(posLoc); gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer); gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0); for (let i = 0; i < iterations; i++) { const nextIdx = 1 - currentTextureIdx; gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[nextIdx]); gl.viewport(0, 0, simWidth, simHeight); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[currentTextureIdx]); // We only want mouse interaction on the first iteration of each frame // to avoid overwhelming the simulation, but it's actually fine to do every time. // However, if we do 8 iterations, we might want to only apply mouse on the first. if (i > 0) { gl.uniform1f(uMousePressLoc, 0.0); } gl.drawArrays(gl.TRIANGLES, 0, 6); currentTextureIdx = nextIdx; } // Render to screen gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height); gl.useProgram(renderProgram); const rResLoc = gl.getUniformLocation(renderProgram, "u_resolution"); gl.uniform2f(rResLoc, canvas.width, canvas.height); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[currentTextureIdx]); const rPosLoc = gl.getAttribLocation(renderProgram, "position"); gl.enableVertexAttribArray(rPosLoc); gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer); gl.vertexAttribPointer(rPosLoc, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame(step); } // Initializing extension for float textures if needed (though WebGL2 usually has it) // In WebGL2, RG32F is standard. window.addEventListener('resize', resize); // Start initWebGL(); updateUI(); requestAnimationFrame(step); </script> </body> </html> ```

TAKE IT TO CODE

This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.

RUN

modelgemma-4-26b-a4b-it
vendorGoogle
harnesspi-agent
generated2026-07-22

METRICS

runtime
tokens in53
tokens out6.9k
cached1.3k
cost / run$0.0023