Reaction-diffusion · gemma-4-31b-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.
transcript · 6 messages
Run a Gray-Scott reaction-diffusion simulation on a canvas that grows organic Turing patterns in real time, with the mouse seeding new growth.
thinking
The user wants a single-page web app (`index.html`) that implements a Gray-Scott reaction-diffusion simulation.
Key requirements:
1. Single file (`index.html`) containing CSS, HTML, and JS.
2. No external resources (CDNs, network requests).
3. Canvas-based simulation.
4. Organic Turing patterns growing in real time.
5. Mouse interaction to seed new growth.
6. The simulation needs to be efficient enough to run in a browser (likely using WebGL for performance, as CPU-based RD on a reasonable grid size is too slow).
Gray-Scott Reaction-Diffusion Basics:
Two substances, $U$ and $V$.
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$
$D_u, D_v$: Diffusion coefficients.
$F$: Feed rate.
$k$: Kill rate.
To make this run in real-time on a canvas, I should use WebGL.
I'll need:
- Two textures for the current state (ping-pong buffering).
- A fragment shader to compute the Laplacian and update the concentrations.
- A fragment shader to render the concentrations to colors.
Plan:
1. HTML structure with a canvas.
2. CSS for full-screen layout.
3. WebGL setup:
- Vertex shader (simple quad).
- Computation fragment shader (Gray-Scott logic).
- Rendering fragment shader (color mapping).
- Framebuffers for ping-ponging.
- Mouse input handling to inject substance $V$.
4. Simulation loop.
Let's refine the parameters for "organic" patterns:
Typical values:
$D_u = 0.209, D_v = 0.105, F = 0.037, k = 0.060$ (can produce coral-like patterns).
Or $F = 0.0545, k = 0.062$ (spots).
I'll provide a few presets or just one good organic one.
Since I can't use external libraries (no gl-matrix, etc.), I'll implement basic vector/matrix helpers if needed, but for a simple quad and textures, I can hardcode the vertex data.
Implementation details:
- Use `Float32Array` for textures if supported, otherwise `UNSIGNED_BYTE` (but RD needs precision, so `HALF_FLOAT` or `FLOAT` is preferred). Actually, I can store $U$ and $V$ in the Red and Green channels of an `RGBA` texture.
- Use `gl.getExtension('OES_texture_float')` or `gl.getExtension('EXT_color_buffer_float')`.
Let's write the code.{"content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-wiValidation failed for tool "write": - path: must have required properties path Received arguments: { "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Gray-Scott Reaction-Diffusion</title>\n <style>\n body, html {\n margin: 0;\n padding: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background-color: #111;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n }\n canvas {\n display: block;\n width: 100vw;\n height: 100vh;\n image-rendering: pixelated;\n cursor: crosshair;\n }\n #ui {\n position: absolute;\n top: 20px;\n left: 20px;\n color: white;\n background: rgba(0, 0, 0, 0.6);\n padding: 15px;\n border-radius: 8px;\n pointer-events: none;\n user-select: none;\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n .hint {\n font-size: 0.8em;\n opacity: 0.8;\n margin-top: 5px;\n }\n h1 {\n margin: 0 0 10px 0;\n font-size: 1.2em;\n font-weight: 500;\n }\n </style>\n</head>\n<body>\n <div id=\"ui\">\n <h1>Turing Patterns</h1>\n <div>Gray-Scott Reaction-Diffusion</div>\n <div class=\"hint\">Click and drag to seed growth</div>\n </div>\n <canvas id=\"glCanvas\"></canvas>\n\n <script>\n const canvas = document.getElementById('glCanvas');\n const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true });\n\n if (!gl) {\n alert('WebGL not supported');\n }\n\n // Ensure float textures are supported\n const floatTexture = gl.getExtension('OES_texture_float');\n const linearTexture = gl.getExtension('OES_texture_float_linear');\n \n if (!floatTexture) {\n alert('Float textures not supported on this browser/device.');\n }\n\n // Simulation parameters\n const params = {\n du: 0.209,\n dv: 0.105,\n feed: 0.037,\n kill: 0.060,\n dt: 1.0\n };\n\n // Shaders\n const vsSource = `\n attribute vec2 a_position;\n varying vec2 v_texCoord;\n void main() {\n v_texCoord = a_position * 0.5 + 0.5;\n gl_Position = vec4(a_position, 0.0, 1.0);\n }\n `;\n\n const simulationFsSource = `\n precision highp float;\n varying vec2 v_texCoord;\n uniform sampler2D u_state;\n uniform vec2 u_resolution;\n uniform vec2 u_mouse;\n uniform bool u_mouseDown;\n\n void main() {\n vec2 texel = 1.0 / u_resolution;\n \n // Sample current state\n // R = U, G = V\n vec2 state = texture2D(u_state, v_texCoord).rg;\n \n float u = state.r;\n float v = state.g;\n\n // Laplacian using 3x3 stencil\n vec2 lap = vec2(0.0);\n lap += texture2D(u_state, v_texCoord + vec2(-texel.x, 0.0)).rg * 0.2;\n lap += texture2D(u_state, v_texCoord + vec2(texel.x, 0.0)).rg * 0.2;\n lap += texture2D(u_state, v_texCoord + vec2(0.0, -texel.y)).rg * 0.2;\n lap += texture2D(u_state, v_texCoord + vec2(0.0, texel.y)).rg * 0.2;\n lap += texture2D(u_state, v_texCoord + vec2(-texel.x, -texel.y)).rg * 0.05;\n lap += texture2D(u_state, v_texCoord + vec2(texel.x, -texel.y)).rg * 0.05;\n lap += texture2D(u_state, v_texCoord + vec2(-texel.x, texel.y)).rg * 0.05;\n lap += texture2D(u_state, v_texCoord + vec2(texel.x, texel.y)).rg * 0.05;\n lap -= state;\n\n float du = 0.209;\n float dv = 0.105;\n float feed = 0.037;\n float kill = 0.060;\n\n float reaction = u * v * v;\n float nextU = u + (du * lap.r - reaction + feed * (1.0 - u));\n float nextV = v + (dv * lap.g + reaction - (feed + kill) * v);\n\n // Mouse interaction: seed substance V\n if (u_mouseDown) {\n float dist = distance(v_texCoord, u_mouse);\n if (dist < 0.01) {\n nextV = 0.9;\n }\n }\n\n gl_FragColor = vec4(clamp(nextU, 0.0, 1.0), clamp(nextV, 0.0, 1.0), 0.0, 1.0);\n }\n `;\n\n const renderFsSource = `\n precision highp float;\n varying vec2 v_texCoord;\n uniform sampler2D u_state;\n\n void main() {\n vec2 state = texture2D(u_state, v_texCoord).rg;\n float val = state.g; // Use V for visualization\n \n // Color mapping: Organic blue/purple/white\n vec3 color1 = vec3(0.05, 0.05, 0.1); // Dark bg\n vec3 color2 = vec3(0.2, 0.4, 0.8); // Mid blue\n vec3 color3 = vec3(0.9, 0.9, 1.0); // Light highlight\n \n vec3 color = mix(color1, color2, smoothstep(0.0, 0.3, val));\n color = mix(color, color3, smoothstep(0.3, 0.6, val));\n \n gl_FragColor = vec4(color, 1.0);\n }\n `;\n\n function createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error(gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n }\n\n function createProgram(gl, vsSource, fsSource) {\n const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);\n const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);\n const program = gl.createProgram();\n gl.attachShader(program, vs);\n gl.attachShader(program, fs);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error(gl.getProgramInfoLog(program));\n return null;\n }\n return program;\n }\n\n const simProgram = createProgram(gl, vsSource, simulationFsSource);\n const renderProgram = createProgram(gl, vsSource, renderFsSource);\n\n const positionBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);\n\n let width, height;\n let textureA, textureB, fbA, fbB;\n let currentA = true;\n\n function createTexture() {\n const tex = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n return tex;\n }\n\n function createFramebuffer(tex) {\n const fb = gl.createFramebuffer();\n gl.bindFramebuffer(gl.FRAMEBUFFER, fb);\n gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);\n return fb;\n }\n\n function resize() {\n width = Math.floor(window.innerWidth / 2); // Run at half res for speed, upscale via linear interpolation\n height = Math.floor(window.innerHeight / 2);\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n\n if (textureA) {\n gl.deleteTexture(textureA);\n gl.deleteTexture(textureB);\n gl.deleteFramebuffer(fbA);\n gl.deleteFramebuffer(fbB);\n }\n\n textureA = createTexture();\n textureB = createTexture();\n fbA = createFramebuffer(textureA);\n fbB = createFramebuffer(textureB);\n\n // Initialize with U=1.0, V=0.0\n const initialData = new Float32Array(width * height * 4);\n for (let i = 0; i < width * height * 4; i += 4) {\n initialData[i] = 1.0;\n initialData[i + 1] = 0.0;\n initialData[i + 2] = 0.0;\n initialData[i + 3] = 1.0;\n }\n gl.bindTexture(gl.TEXTURE_2D, textureA);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, initialData);\n }\n\n window.addEventListener('resize', resize);\n resize();\n\n let mouseX = 0, mouseY = 0, mouseDown = false;\n canvas.addEventListener('mousedown', () => mouseDown = true);\n window.addEventListener('mouseup', () => mouseDown = false);\n canvas.addEventListener('mousemove', (e) => {\n mouseX = e.clientX / window.innerWidth;\n mouseY = 1.0 - (e.clientY / window.innerHeight);\n });\n\n function step() {\n const readTex = currentA ? textureA : textureB;\n const writeFb = currentA ? fbB : fbA;\n\n gl.useProgram(simProgram);\n \n const posLoc = gl.getAttribLocation(simProgram, 'a_position');\n gl.enableVertexAttribArray(posLoc);\n gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);\n\n gl.uniform2f(gl.getUniformLocation(simProgram, 'u_resolution'), width, height);\n gl.uniform2f(gl.getUniformLocation(simProgram, 'u_mouse'), mouseX, mouseY);\n gl.uniform1i(gl.getUniformLocation(simProgram, 'u_mouseDown'), mouseDown ? 1 : 0);\n \n gl.bindFramebuffer(gl.FRAMEBUFFER, writeFb);\n gl.viewport(0, 0, width, height);\n gl.bindTexture(gl.TEXTURE_2D, readTex);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n currentA = !currentA;\n }\n\n function render() {\n const readTex = currentA ? textureA : textureB;\n gl.useProgram(renderProgram);\n \n const posLoc = gl.getAttribLocation(renderProgram, 'a_position');\n gl.enableVertexAttribArray(posLoc);\n gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);\n gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);\n\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.bindTexture(gl.TEXTURE_2D, readTex);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n }\n\n function loop() {\n // Multiple simulation steps per frame for speed\n for (let i = 0; i < 8; i++) {\n step();\n }\n render();\n requestAnimationFrame(loop);\n }\n\n // Seed a small starting patch\n const seedData = new Float32Array(width * height * 4);\n for (let i = 0; i < width * height * 4; i += 4) {\n seedData[i] = 1.0;\n seedData[i + 1] = 0.0;\n seedData[i + 2] = 0.0;\n seedData[i + 3] = 1.0;\n }\n // Center spot\n for (let y = Math.floor(height/2)-10; y < Math.floor(height/2)+10; y++) {\n for (let x = Math.floor(width/2)-10; x < Math.floor(width/2)+10; x++) {\n const idx = (y * width + x) * 4;\n seedData[idx + 1] = 1.0;\n }\n }\n gl.bindTexture(gl.TEXTURE_2D, textureA);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, seedData);\n\n loop();\n </script>\n</body>\n</html>\n" }
thinking
I forgot to include the `path` parameter in the `write` call. I will fix this now.
index.html (12028 bytes)Successfully wrote 12028 bytes to index.html
I have created a self-contained single-page web application that implements a Gray-Scott reaction-diffusion simulation. The simulation uses WebGL to perform the heavy computations on the GPU, allowing for real-time growth of organic, Turing-like patterns. I've implemented a double-buffering system with floating-point textures to maintain the precision required for the diffusion process. **Key features of the implementation:** - **Real-time GPU Simulation:** The Laplacian and reaction terms are computed in a fragment shader, allowing for high performance. - **Interactive Seeding:** You can click and drag your mouse across the canvas to introduce "substance V," which seeds new organic growth. - **Organic Aesthetics:** The rendering pass maps the chemical concentrations to a deep blue and white color palette for a biological, fluid look. - **Optimized Performance:** The simulation runs at half-resolution and performs multiple integration steps per frame to ensure the patterns evolve at a visually satisfying speed. The complete application is contained within `index.html`.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Gray-Scott reaction-diffusion grows organic maze-like patterns over time, reacting to drag, wheel, and keyboard input.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.