Reaction-diffusion · step-3.5-flash
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 · 4 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 me to create a single-page web app that implements a Gray-Scott reaction-diffusion simulation. This is a classic algorithmic art / emergent pattern system that creates organic Turing patterns.
Key requirements:
1. Single self-contained file: index.html
2. CSS in <style> tag
3. JS in <script> tag
4. No external resources, CDNs, or network requests
5. Runs fully sandboxed
6. Gray-Scott reaction-diffusion simulation
7. Real-time growth
8. Mouse interaction to seed new growth
Let me think about what I need to implement:
**Gray-Scott Model:**
- Two chemicals: A and B
- Diffusion rates: D_A, D_B
- Feed rate (f) and kill rate (k)
- Partial differential equations:
∂A/∂t = D_A∇²A - AB² + f(1-A)
∂B/∂t = D_B∇²B + AB² - (f+k)B
**Implementation approach:**
1. Canvas element for rendering
2. Web Worker for simulation (to avoid blocking UI)
3. Mouse interaction - clicking adds B chemical at that location
4. Real-time animation loop
5. Proper simulation parameters
Since we need a single file, I'll embed everything in index.html:
- HTML structure
- CSS for full-screen canvas
- JavaScript:
- Canvas setup
- Simulation logic (I can use a simple approach with a 2D grid)
- Mouse event handlers
- Animation loop
For performance, I'll use a simple 2D array approach with a smaller internal simulation resolution and scale up with CSS image-rendering: pixelated for that retro/organic feel.
Let me write a complete implementation:
```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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100%;
height: 100%;
display: block;
}
.controls {
position: fixed;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
padding: 10px;
border-radius: 8px;
color: #fff;
font-family: monospace;
font-size: 12px;
z-index: 100;
}
.controls input {
width: 60px;
}
</style>
</head>
<body>
<canvas id="sim"></canvas>
<div class="controls">
<div>Feed: <input id="feed" type="range" min="0.01" max="0.1" step="0.001" value="0.055"></div>
<div>Kill: <input id="kill" type="range" min="0.03" max="0.07" step="0.001" value="0.062"></div>
<div>Speed: <input id="speed" type="range" min="1" max="50" step="1" value="10"></div>
<div style="margin-top:8px;">Click to add seeds</div>
</div>
<script>
// Canvas setup
const canvas = document.getElementById('sim');
const ctx = canvas.getContext('2d');
let width, height;
const scale = 4; // Scale up the simulation for performance
function resize() {
width = Math.floor(window.innerWidth / scale);
height = Math.floor(window.innerHeight / scale);
canvas.width = width;
canvas.height = height;
}
resize();
window.addEventListener('resize', resize);
// Simulation parameters
let feedRate = 0.055;
let killRate = 0.062;
let iterationsPerFrame = 10;
// Grid arrays
let gridA, gridB, nextA, nextB;
function initGrids() {
gridA = new Float32Array(width * height);
gridB = new Float32Array(width * height);
nextA = new Float32Array(width * height);
nextB = new Float32Array(width * height);
// Initialize with A=1 everywhere, B=0 everywhere
for (let i = 0; i < width * height; i++) {
gridA[i] = 1;
gridB[i] = 0;
}
// Add some initial seeds in the center
const cx = Math.floor(width / 2);
const cy = Math.floor(height / 2);
const seedRadius = 5;
for (let dy = -seedRadius; dy <= seedRadius; dy++) {
for (let dx = -seedRadius; dx <= seedRadius; dx++) {
if (dx*dx + dy*dy <= seedRadius*seedRadius) {
const x = cx + dx;
const y = cy + dy;
if (x >= 0 && x < width && y >= 0 && y < height) {
gridB[y * width + x] = 1;
}
}
}
}
}
initGrids();
// Laplacian convolution weights
// 3x3 Laplacian with diagonals weighted 0.05
// Actually the standard discrete Laplacian using 4 or 8 neighbors
// Using 4-neighbor (simpler) or 8-neighbor for smoother diffusion
// Let's use the standard 3x3 kernel:
// 0.05 0.2 0.05
// 0.2 -1.0 0.2
// 0.05 0.2 0.05
// Or simplified 4-neighbor: center -1, neighbors 0.25
// I'll use a slightly more realistic one from common Gray-Scott implementations:
// weights: [0.05, 0.2, 0.05, 0.2, -1, 0.2, 0.05, 0.2, 0.05]
const laplacianWeights = [
0.05, 0.2, 0.05,
0.2, -1.0, 0.2,
0.05, 0.2, 0.05
];
function getIdx(x, y) {
// Wrapped boundaries (toroidal)
if (x < 0) x = width - 1;
if (x >= width) x = 0;
if (y < 0) y = height - 1;
if (y >= height) y = 0;
return y * width + x;
}
function computeLaplacian(x, y, grid) {
let sum = 0;
const w = width;
const h = height;
// Neighbors in 3x3 with weights
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = (x + dx + w) % w;
const ny = (y + dy + h) % h;
const weight = laplacianWeights[(dy + 1) * 3 + (dx + 1)];
sum += grid[ny * w + nx] * weight;
}
}
return sum;
}
function simulateStep() {
const w = width, h = height;
const D_A = 1.0;
const D_B = 0.5;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = y * w + x;
const a = gridA[idx];
const b = gridB[idx];
const lapA = computeLaplacian(x, y, gridA);
const lapB = computeLaplacian(x, y, gridB);
// Gray-Scott equations
const reaction = a * b * b;
nextA[idx] = a + D_A * lapA - reaction + feedRate * (1 - a);
nextB[idx] = b + D_B * lapB + reaction - (feedRate + killRate) * b;
// Clamp values to [0, 1]
nextA[idx] = Math.max(0, Math.min(1, nextA[idx]));
nextB[idx] = Math.max(0, Math.min(1, nextB[idx]));
}
}
// Swap grids
const tempA = gridA;
const tempB = gridB;
gridA = nextA;
gridB = nextB;
nextA = tempA;
nextB = tempB;
}
function render() {
const imgData = ctx.createImageData(width, height);
const data = imgData.data;
for (let i = 0; i < gridB.length; i++) {
const b = gridB[i];
const a = gridA[i];
// Color based on B concentration
// High B = white/bright, Low B = dark
// For more organic look, we can use a palette
// Simple: intensity based on B
const val = Math.floor(b * 255);
// Some organic tone: more interesting than grayscale?
// Let's do a blue-ish or greenish tint
data[i * 4 + 0] = val * 0.5; // R
data[i * 4 + 1] = val; // G (more green)
data[i * 4 + 2] = val * 1.2; // B (more blue)
data[i * 4 + 3] = 255; // A
}
ctx.putImageData(imgData, 0, 0);
}
// Animation loop
function animate() {
for (let i = 0; i < iterationsPerFrame; i++) {
simulateStep();
}
render();
requestAnimationFrame(animate);
}
// Mouse interaction
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = Math.floor((e.clientX - rect.left) * scaleX);
const y = Math.floor((e.clientY - rect.top) * scaleY);
// Add a cluster of B
const radius = 3;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (dx*dx + dy*dy <= radius*radius) {
const px = x + dx;
const py = y + dy;
if (px >= 0 && px < width && py >= 0 && py < height) {
gridB[py * width + px] = 1;
}
}
}
}
});
// Control handlers
document.getElementById('feed').addEventListener('input', (e) => {
feedRate = parseFloat(e.target.value);
});
document.getElementById('kill').addEventListener('input', (e) => {
killRate = parseFloat(e.target.value);
});
document.getElementById('speed').addEventListener('input', (e) => {
iterationsPerFrame = parseInt(e.target.value);
});
// Start animation
animate();
</script>
</body>
</html>
```
This looks good but wait - we need to make sure there are NO external resources. All good, it's all inline.
Also performance: using a Float32Array and a small simulation grid (scaled down) with canvas scaling via sizes is efficient.
The laplacian weights I used are common for Gray-Scott: the 3x3 kernel with center -1 and weighted neighbors gives a more isotropic diffusion. That's good.
Let me test the simulation mentally:
- We have feedRate and killRate as parameters
- Initial condition: A=1, B=0 everywhere, with a small seed of B=1 in center
- Each step computes laplacian, then updates A and B per equations
- Clamping keeps values in [0,1]
- Render shows B concentration as color
I should also consider that the mouse interaction might create isolated spots that grow into patterns. Good.
Let's refine the visual a bit more for better aesthetics. Instead of simple blue-green tint, maybe use a more organic palette that varies with both A and B? Or we could use a color map. The classic Gray-Scott visualizations often use B concentration with a colormap like plasma or inferno. Since we can't use external libraries, I'll create a simple organic color mapping.
Maybe:
- B high: cell-like organic colors (reds, oranges, purples)
- B low: dark blue/black background
But simpler is fine.
I'll also add a slight improvement: use a more stable time step? Actually the simulation can be unstable if parameters cause values to blow up, but clamping helps.
Let's enhance the rendering to be more visually appealing with a nice organic color scheme:
```javascript
function render() {
const imgData = ctx.createImageData(width, height);
const data = imgData.data;
for (let i = 0; i < gridB.length; i++) {
const b = gridB[i];
const a = gridA[i];
// Use B concentration as primary driver, but also incorporate A for variation
// Create organic colors: brownish/reddish for low B, cyan/green/blue for medium, purple for high?
// Actually let's do a cellular organic look:
// Background: very dark
// Pattern colors: various earth tones
if (b < 0.01) {
// Background
data[i*4+0] = 5;
data[i*4+1] = 5;
data[i*4+2] = 10;
} else if (b < 0.2) {
const t = b / 0.2;
// Earthy brown to green
data[i*4+0] = Math.floor(80 + t*80);
data[i*4+1] = Math.floor(50 + t*100);
data[i*4+2] = Math.floor(30);
} else if (b < 0.5) {
const t = (b - 0.2) / 0.3;
// Green to cyan
data[i*4+0] = Math.floor(50 - t*30);
data[i*4+1] = Math.floor(150 + t*50);
data[i*4+2] = Math.floor(30 + t*100);
} else {
const t = (b - 0.5) / 0.5;
// Cyan to purple/blue
data[i*4+0] = Math.floor(20 - t*10);
data[i*4+1] = Math.floor(100 + t*50);
data[i*4+2] = Math.floor(130 + t*100);
}
data[i*4+3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}
```
But maybe simpler grayscale with a nice scaling is better? Let's keep it simpler but effective:
```javascript
function render() {
const imgData = ctx.createImageData(width, height);
const data = imgData.data;
for (let i = 0; i < gridB.length; i++) {
const b = gridB[i];
// Use a sigmoid-like curve to emphasize patterns
const v = Math.pow(b, 0.5) * 255;
data[i*4+0] = v * 0.4; // R
data[i*4+1] = v * 0.9; // G (greenish)
data[i*4+2] = v * 1.0; // B
data[i*4+3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}
```
Actually, let's produce a more organic look that varies with B, with perhaps a slightly variable color to avoid looking too uniform. I'll use a palette that gives a biological, cell-like appearance with different tones.
Let me create a more refined version:
```javascript
function render() {
const imgData = ctx.createImageData(width, height);
const data = imgData.data;
for (let i = 0; i < gridB.length; i++) {
const b = gridB[i];
const a = gridA[i];
// Intensity based on B concentration
let intensity = b;
// Gamma correction for better contrast
intensity = Math.pow(intensity, 0.8);
// Map to an organic color palette
let r, g, b_val;
if (intensity < 0.1) {
// Very dark background
r = 5;
g = 8;
b_val = 12;
} else if (intensity < 0.3) {
// Dark organic brown
const t = (intensity - 0.1) / 0.2;
r = Math.floor(30 + t * 60);
g = Math.floor(25 + t * 40);
b_val = Math.floor(20 + t * 20);
} else if (intensity < 0.6) {
// Greenish/teal (cells)
const t = (intensity - 0.3) / 0.3;
r = Math.floor(90 - t * 40);
g = Math.floor(65 + t * 90);
b_val = Math.floor(40 + t * 70);
} else {
// Brighter cyan/blue with white highlights
const t = (intensity - 0.6) / 0.4;
r = Math.floor(50 + t * 100);
g = Math.floor(155 + t * 100);
b_val = Math.floor(110 + t * 145);
}
const idx = i * 4;
data[idx + 0] = r;
data[idx + 1] = g;
data[idx + 2] = b_val;
data[idx + 3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}
```
This gives a nice range: dark background, then earthy brownish/organic colors, then greenish cells, then bright cyan/blue.
Alternatively, maybe a simpler approach: use B as grayscale but apply a colormap. But I think it's fine.
Also note: I'm using a toroidal (wrapped) boundary. That's common for these simulations. Good.
Let's ensure performance: The simulation runs at width x height resolution. With scale=4, if the screen is 1920x1080, that's 480x270 = ~130k cells. That's fine. And we do iterationsPerFrame steps each frame. That should be okay on modern devices. If needed we could use a smaller scale or use WebGL for better performance, but that would add complexity. The CPU approach is fine for moderate resolutions.
Potential improvements:
- Use 1D array is more cache-friendly
- Use typed arrays already
- We could remove the createImageData allocation each frame by reusing, but that's minor.
Actually, creating ImageData each frame can be expensive. Better to create it once and reuse:
```javascript
let imgData;
function initRender() {
imgData = ctx.createImageData(width, height);
}
```
But width/height change on resize. So we need to recreate on resize.
Anyway, for simplicity I'll keep as is or optimize slightly:
Let's restructure:
- Create imgData once, reuse if same dimensions. But on resize we need to recreate.
- That's okay.
But to keep code clean, I'll do:
```javascript
let imageData = ctx.createImageData(width, height);
const data = imageData.data;
function render() {
// fill data
ctx.putImageData(imageData, 0, 0);
}
```
And on resize, reinitialize imageData.
I'll incorporate that.
Also, we need to ensure the controls are visible but not interfering. They are in a div fixed at top-left with semi-transparent background. Good.
One more thing: We should handle the case where feedRate and killRate produce stable patterns. Common parameters:
- Feed: 0.055, Kill: 0.062 gives spots
- Feed: 0.037, Kill: 0.06 gives waves
- Feed: 0.035, Kill: 0.065 gives stripes
I'll set defaults to something interesting.
Set feedRate default: 0.055, killRate: 0.062. That yields spots.
But also we might want to randomize seeds? The user can click to add seeds. That's good.
Edge case: simulation could die out if feedRate is too low or killRate too high. That's fine, user can adjust.
Let's also consider the laplacian weights. Some implementations use a simpler 5-point stencil: center -1, neighbors 0.25. But 8-neighbor gives more isotropic diffusion. The weights I used sum to 1? Let's check: sum of weights: 0.05+0.2+0.05 +0.2 + (-1) +0.2 +0.05+0.2+0.05 = 0.0? 0.05+0.2=0.25, +0.05=0.3, +0.2=0.5, +(-1)=-0.5, +0.2=-0.3, +0.05=-0.25, +0.2=-0.05, +0.05=0.0. Yes, they sum to 0, which is typical for Laplacian kernels. Good.
Now, write the final code. I'll produce a final single HTML file.
But we need to make sure it's truly one file and runs without any external dependencies. All good.
Let's write the final answer as the content of index.html.
I'll also add a small instruction overlay that fades out? Not necessary. The controls provide info.
Also consider: Use requestAnimationFrame for smooth animation. That's fine.
Add some more interactive features? Maybe right-click to clear? Not asked, just mouse seeding. I'll stick to click to add B.
I'll add a "clear" button? Not needed.
Also consider the simulation might need stabilization at start. The initial seed in center should produce patterns.
Let's finalize:
HTML structure: canvas, controls div with sliders.
CSS: full-screen canvas, button styling.
JS: all the simulation as described.
Write everything into index.html in current directory.
Now produce final answer with file content.I'll create a self-contained Gray-Scott reaction-diffusion simulation with organic Turing patterns and mouse interaction. The entire application will be in a single HTML file with inline CSS and JavaScript.
index.html (9177 bytes)Successfully wrote 9177 bytes to index.html
thinking
The file has been written successfully. I've created a self-contained Gray-Scott reaction-diffusion simulation in a single HTML file. Let me verify that it's correct and complete: 1. ✓ Single HTML file with inline CSS and JavaScript 2. ✓ No external resources, CDNs, or network requests 3. ✓ Gray-Scott reaction-diffusion algorithm implemented 4. ✓ Real-time simulation with `requestAnimationFrame` 5. ✓ Organic color palette based on B concentration (dark background → organic brown → moss green → teal cyan → pale blue/white) 6. ✓ Mouse click/touch seeding of new growth 7. ✓ Controls for feed rate, kill rate, and simulation speed 8. ✓ Responsive canvas that adapts to window size 9. ✓ Toroidal boundary conditions (wrap-around) 10. ✓ Proper diffusion using laplacian with 8-neighbor weights 11. ✓ Clamping of concentrations to [0,1] 12. ✓ Test default parameters (0.055, 0.062) produce nice patterns The simulation should produce beautiful organic Turing patterns that evolve over time. The user can click anywhere to seed new growth, and adjust the parameters to explore different pattern regimes.
I've created a self-contained Gray-Scott reaction-diffusion simulation in `index.html`. The file includes: - **Gray-Scott algorithm** with proper diffusion and reaction terms - **Organic color palette** mapping B concentration to earthy browns → moss greens → teal cyans → pale blues/whites - **Real-time rendering** with parametrizable iterations per frame - **Mouse/touch interaction** to seed new growth anywhere - **Control panel** to adjust feed rate, kill rate, and simulation speed - **Responsive design** adapting to window size - **Toroidal boundaries** (wrap-around) for continuous patterns The simulation starts with a central seed and grows Turing patterns organically. Click anywhere to inject new "B" and watch new growth emerge from that point. Adjust feed/kill parameters to see drastically different pattern morphologies (spots, stripes, waves, chaos).
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 Turing patterns automatically; click and drag seed additional growth, filling the canvas.
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.