oneshotlm
Compare all models on this prompt →

Reaction-diffusion · kimi-k2.7-code

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

⛨ sandboxed Open in new tab ↗
transcript · 6 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
 We need create index.html with Gray-Scott reaction-diffusion sim on canvas, mouse seeds new growth, organic Turing patterns real time, no external resources. Must be single self-contained file in /workspace/index.html.

We should implement efficient GPU-ish? In JS CPU with two float arrays. Use canvas putImageData. Gray-Scott parameters: feed, kill. Use GrayScott equations with diffusion, dt, etc. Use 2D grid maybe 256x256 or 512x512 depending performance. Use mouse seeding adds activator A/B. Also preseed patterns? Growth over time. Use color mapping nice organic colors. Possibly use feed/kill values forming spots/coral patterns: F=0.0545, K=0.062 (coral). Or transition. Maybe allow parameter drift / multiple presets. Need render at high frame.

Implement simulation object with grid sizes 256x256 maybe because update full grid each frame. Could use Float32Arrays for U and V. Diffusion: laplacian weighted 5-point with 0.2 center? Standard: delta U = Du*laplace(U) - u*v^2 + f*(1-u), delta V = Dv*laplace(V) + u*v^2 - (f+k)*v. Use Du=0.16, Dv=0.08; dt=1.

Performance: For each cell compute neighbors indices. Could use typed arrays. For 256x256, 65k cells, each frame 2 arrays ~ ok 60fps. Could scale canvas to window. Use low resolution simulation rendered scaled via CSS; canvas actual width/height = grid or scaled? Use sim resolution 300x300 maybe. CSS scale to full window. Use requestAnimationFrame.

Need mouse seeding: on mousedown/drag set radius around cursor with high v. Coordinate map from screen to grid.

Color mapping: map u/v to RGBA. Precompute palette maybe. Render U/V into ImageData. Use shades based on v, maybe teal/gold. Use direct computation.

We can add controls? User didn't ask. Maybe simple overlay with instructions and parameter display. Keep minimal. Auto random seed in center initially. Seed at mouse. Seed function sets u=0.5, v=0.25 within radius.

Maybe use a init pattern filled with small random noise so patterns grow. We can add seed in center.

We need ensure single file. We'll create /workspace/index.html. Use write.

Let's design code carefully:

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; }
  html, body { width:100%; height:100%; overflow:hidden; background:#050505; }
  canvas { display:block; width:100vw; height:100vh; image-rendering:pixelated; }
  #info { position:fixed; top:10px; left:10px; color:rgba(255,255,255,.7); font-family:sans-serif; font-size:13px; pointer-events:none; user-select:none; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="info">Gray-Scott reaction-diffusion<br>Drag to seed organic growth</div>
<script>

JS:
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

const W=320, H=320; 
canvas.width=W; canvas.height=H;

const size=W*H;
const u=new Float32Array(size);
const v=new Float32Array(size);
const u2=new Float32Array(size);
const v2=new Float32Array(size);

function idx(x,y){return y*W+x;}

// init state
function reset() {
  for(let i=0;i<size;i++){ u[i]=1; v[i]=0; }
  seed(W/2, H/2, 12);
}
function seed(cx,cy,r){
  for(let y=Math.max(0,cy-r|0); y<=Math.min(H-1,cy+r|0); y++){
    for(let x=Math.max(0,cx-r|0); x<=Math.min(W-1,cx+r|0); x++){
      const dx=x-cx, dy=y-cy;
      if(dx*dx+dy*dy<=r*r){
        const i=idx(x,y);
        u[i]=0.5; v[i]=0.25;
      }
    }
  }
}

// add noise
function noiseSeed() {
  for(let i=0;i<size;i++){
    if(Math.random()<0.002){
      u[i]=0.5; v[i]=0.25;
    }
  }
}

reset();
noiseSeed();

const params = [
  {f:0.0545,k:0.0620,name:'coral'},
  {f:0.037,k:0.06,name:'spots'},
  {f:0.025,k:0.06,name:'mazes'},
];
let p = params[0];

function laplace(arr, x, y) {
  const i = idx(x,y);
  // wrap? use clamp? Organic with clamp maybe zero-flux. Use wrap.
  const up = (y-1+H)%H;
  const down = (y+1)%H;
  const left = (x-1+W)%W;
  const right = (x+1)%W;
  return arr[idx(x,up)]*0.2 + arr[idx(x,down)]*0.2 + arr[idx(left,y)]*0.2 + arr[idx(right,y)]*0.2 + arr[i]*0.2;
}
Actually standard laplacian with center -1? Formula above: sum neighbors*0.2 + center*0.2 -?? The diffused = 0.2*center +0.2*4neighbors? That approximates laplace*? Standard: lap = (sum of 4 neighbors - 4*center)/4 = 0.25*sumN - center. Equivalent to using weights center -1, neighbors 0.25. If using center 0.2 and neighbors 0.2 each, result = 0.2*center+0.8*avgN? Not correct. Need compute lap = (N+S+E+W - 4*C)/4. But multiplications each step Du*lap. We can inline neighbors.

Update:
for y 0..H, for x 0..W:
  const i = y*W+x;
  const uC = u[i];
  const vC = v[i];
  const uLap = (u[i-1?]+u[i+1?]+u[i-W]+u[i+W] - 4*uC)/4;
  ...

Need handle edges. Could use toroidal, but index boundaries for x. Better compute row indexes. For x edges wrap using neighbor indexes. Use if else.

Optimization: Precompute neighbor offsets? For each i, left = x>0?i-1:i+W-1, right = x<W-1?i+1:i-W+1, up = i-W (with y>0? else size-W), down = i+W (y<H-1? else W-H). But branching per cell. W*H 100k fine.

Update loop:
for(let y=0;y<H;y++){
  const row = y*W;
  for(let x=0;x<W;x++){
    const i=row+x;
    let uL = x===0? i+W-1 : i-1;
    let uR = x===W-1? i-W+1 : i+1;
    let uU = y===0? i + size - W : i - W;
    let uD = y===H-1? i - size + W : i + W;
    const uC = u[i], vC = v[i];
    const uLap = (u[uL]+u[uR]+u[uU]+u[uD]-4*uC)/4;
    const vLap = (v[uL]+v[uR]+v[uU]+v[uD]-4*vC)/4;
    const uvv = uC*vC*vC;
    u2[i] = uC + (0.16*uLap - uvv + p.f*(1-uC));
    v2[i] = vC + (0.08*vLap + uvv - (p.k+p.f)*vC);
    // clamp
    if(u2[i]<0)u2[i]=0; else if(u2[i]>1)u2[i]=1;
    if(v2[i]<0)v2[i]=0; else if(v2[i]>1)v2[i]=1;
  }
}

Then swap u,u2 and v,v2; render u maybe.

But can we use dt=1? Yes with f, k small. Could also multiply dt 1.5 for speed. For 320x320 maybe okay. We can run multiple steps per frame to grow faster: steps=4? Might be heavy. Use maybe 2 steps per frame.

Render: Create ImageData. Map uC and vC to color. Options:
- Use concentration v to drive color. HSL maybe lerp through palette. Precompute rgba in 256x256 palette based on (u*255)|(v*255) index (65536 entries). That's large but manageable 262KB. Or compute per pixel simple. Use color palette: if algae organic: low v -> dark; high v -> bright lime/cyan/orange. Let's make palette function.

Potential palette function:
function colorFor(uC,vC){
  const pigment = Math.min(1, vC*3.5 + uC*0.2);
  const r = pigment * (0.8 + 0.2*Math.sin(pigment*Math.PI));
  const g = pigment * (0.9 + 0.1*Math.cos(pigment*Math.PI*0.5));
  const b = pigment * pigment * 0.4;
  // convert to bytes.
}
But we can approximate.

Better: Use HSLA? To fill ImageData quickly, manually:
const pal = new Uint32Array(65536); // ABGR for little-endian? Canvas uses RGBA order typed Uint8Clamped. Uint32 view set expects 0xAABBGGRR on little endian for RGBA? Actually Uint32 on little-endian: 0xAABBGGRR gives bytes [RR,GG,BB,AA]. So build ABGR.

Build palette: for iU 0..255, iV 0..255, compute based on v/iV. Maybe palette index = (iV<<8)|iU or iU*256+iV. 

Construct:
const u = iU/255; const v = iV/255;
const t = Math.min(1, v*3 + u*0.3); // pigment
// organic color: dark brown substrate to teal/gold living.
// Map t: 0 -> 5,8,10; mid -> 58,105,70; high -> 255,215,100.
Better use a procedural 3-color gradient.
r = t*(0.8 + 0.2*sin...)? Let's design a nice palette:
c0 = [10,12,14]
c1 = [20,80,90] // cyan/green at t~0.3
c2 = [255,220,80] // gold at t~0.7
c3 = [250,90,100] // pink/red peaks

We can smoothstep interpolation. Function lerp(a,b,t) returns composed color. Use smoothstep. t = v*2.5 clamp. Then for t intervals.

Simple palette:
const amount = Math.min(1, v*2.5 + u*0.2);
const r = amount * (240 + 40*Math.sin(amount * Math.PI*2));
const g = amount * (200 + 30*Math.cos(amount * Math.PI));
const b = amount*amount * 80;
This yields tan/gold. But let's be more systematic.

We can attempt multiple palettes and cycle. But user just wants organic Turing patterns.

We can include parameter controls via keyboard: F switch pattern, R reset, P pause. Maybe helpful. But minimal. No UI controls necessary. But code can include keyboard.

Mouse seeding: map mouse to grid. Need canvas display size vs actual pixel size. Use getBoundingClientRect. Use pointermove when pointer down.

Events: pointerdown set drawing=true; seed at current. pointermove if drawing seed. pointerup end. touch.

I've asked to use tool. Let's create file.

Performance concerns: 320x320 with palette generation and 1 step per frame. requestAnimationFrame. Let's compute maybe ~100k cells, OK in modern JS. Use Float32Array. Render via putImageData.

Edge wrap: use mod. Since W and H constants, branches fine.

Potential issue: The simulation may blow up with dt=1 if f high? Standard F=0.0545,K=0.062 stable. Use dt=1. Good.

We can also add a slow auto random seed every few seconds to keep growth. Maybe not needed; user seeds with mouse. Center initial seed. Add occasional tiny noise to simulate ambient growth.

Let's write robust palette builder.

Palette approach:
const PAL = new Uint32Array(256*256*256? no, index by u8v8). Use index = (iV<<8)|iU. Build using HSL.

Implement rgbToAbgr(r,g,b,a) returns (a<<24)|(b<<16)|(g<<8)|r. For little-endian `Uint8ClampedArray` view, this writes [r,g,b,a] because 0xAABBGGRR -> bytes: RR,GG,BB,AA. Good.

Construct:
for(let iv=0; iv<256; iv++){
  for(let iu=0; iu<256; iu++){
    const u = iu/255;
    const v = iv/255;
    const t = Math.min(1, v*2.8 + u*0.25);
    // colors for substrate, living body, bright tips
    let r,g,b;
    if(t < 0.5){
      // between dark and teal
      const k = t/0.5;
      r = lerp(8, 40, k);
      g = lerp(10, 120, k);
      b = lerp(12, 110, k);
    } else {
      const k = (t-0.5)/0.5;
      r = lerp(40, 255, k);
      g = lerp(120, 230, k);
      b = lerp(110, 70, k);
    }
    const a = 255;
    PAL[(iv<<8)|iu] = (a<<24)|(b<<16)|(g<<8)|r;
  }
}

But Uint32 for palette. Access imageData32[y*W+x] = PAL[(vByte<<8)|uByte]. Need uByte=(u*255)|0 etc with clamp.

But careful: (iv<<8)|iu where iv <= 255, index in 0..65535. Good.

Now render loop:
const img = ctx.createImageData(W,H);
const data32 = new Uint32Array(img.data.buffer);
function render(){
  for(let i=0;i<size;i++){
    const a = u[i]; const b = v[i]; // keep variables
    const ui = (a*255)|0;
    const vi = (b*255)|0;
    data32[i] = PAL[(vi<<8)|ui];
  }
  ctx.putImageData(img,0,0);
}

But we can combine update and render to avoid an extra loop? We need swap after update. We can update multiple steps then render.

Possible optimize: render after each update? Actually only render final step. Good.

Need handle center initial seed. mouse seed size maybe radius 6.

Now code details: multiple reaction parameters. Use current p. Provide keyboard number keys 1-3. Also 'r' reset. Also space pause. Add small control text.

Need maybe use pointer events because touch support. canvas.addEventListener('pointerdown', e=>{ drawing=true; seedEvt(e);});
Pointer events may have pressure. Need e.preventDefault to avoid scrolling. We'll set touch-action:none in CSS.

Mapping:
function seedEvt(e){
  const rect=canvas.getBoundingClientRect();
  const sx = (e.clientX - rect.left) / rect.width * W;
  const sy = (e.clientY - rect.top) / rect.height * H;
  seed(sx,sy,7);
}

seed takes floats. In seed function uses let y=Math.floor(Math.max(0,cy-r)); etc. radius int? Use r float. Use dx*dx+dy*dy <= r*r. But we need integer iterations. We'll use Math.ceil.

function seed(cx,cy,r){
  const r2=r*r;
  const x0=Math.max(0,Math.floor(cx-r));
  const x1=Math.min(W-1,Math.ceil(cx+r));
  const y0=Math.max(0,Math.floor(cy-r));
  const y1=Math.min(H-1,Math.ceil(cy+r));
  for(let y=y0;y<=y1;y++){
    for(let x=x0;x<=x1;x++){
      const dx=x-cx, dy=y-cy;
      if(dx*dx+dy*dy<=r2){
        const i=y*W+x;
        u[i]=0.5+Math.random()*0.05; v[i]=0.25+Math.random()*0.05;
      }
    }
  }
}

Use toroidal wrap if cx near edges? Could wrap coordinates. Not necessary; we'll clamp.

Now update loop. Need implement no-branches neighbor selection for speed? It will pass.

Let's calculate W=320 H=320. PutimgData may be heavy but okay. Use `requestAnimationFrame`. Use dt = 1. Maybe run 2 update steps per frame for faster growth? Let's set stepsPerFrame = 2. That doubles compute ~200k cells, still okay. Could adapt.

But if steps per frame too high may unsmooth. 2 is good. Use `for(let step=0; step<2; step++)` then render once after loop. Good.

Auto parameter drift? Could add subtle F/K drift over time. Not needed. Keyboard switch.

Maybe initial random noise should be small so center seed grows. We can also do initial random patches across canvas so patterns appear even without mouse. But user's request mouse seeds. We'll seed one center and maybe a few random small spots.

Potential code example: fully contained.

Test for syntax: when using float u[i]*255 may exceed 1, clamp to <=255: const ui = Math.min(255,(u[i]*255)|0); use Math.min? If u<1 but because of float maybe 1.000..., bitwise to int 255? `(1.0001*255)|0` = 255? 255.0255|0 = 255. If >1 maybe 255. Fine. v similarly.

ImageData: using ctx.getImageData? We can create once.

Edge indexes: For up when y==0, i - W + size? Actually i=y*W+x; if y==0 up index = (H-1)*W + x = size - W + x = i + size - W. Down when y==H-1 => x = i - size + W. left when x==0 => i+W-1. right when x==W-1 => i-W+1. Correct.

Let's write palette function:
function makePalette(){
  const p=new Uint32Array(256*256);
  for(let iv=0; iv<256; iv++){
    const v=iv/255;
    for(let iu=0; iu<256; iu++){
      const u=iu/255;
      const t=Math.min(1, v*3.0 + u*0.2);
      // organic palette substrate->teal->gold
      let r,g,b;
      if(t<0.35){
        const k=t/0.35;
        r=lerp(4,28,k); g=lerp(5,95,k); b=lerp(7,90,k);
      } else if (t<0.7){
        const k=(t-0.35)/0.35;
        r=lerp(28,190,k); g=lerp(95,210,k); b=lerp(90,80,k);
      } else {
        const k=(t-0.7)/0.3;
        r=lerp(190,255,k); g=lerp(210,245,k); b=lerp(80,50,k);
      }
      p[(iv<<8)|iu]=0xFF000000|((b|0)<<16)|((g|0)<<8)|(r|0); // careful precedence
      // Actually 0xFF000000 sets alpha=255 but if b<<16 can overflow? Use (255<<24)|(b<<16)|(g<<8)|r.
    }
  }
  return p;
}

In JS, `0xFF000000 | (b<<16)` okay because b<=255.

`lerp(a,b,t){ return a + (b-a)*t; }`.

Now complete. Let's also add a star of `ctx.imageSmoothingEnabled=false` not needed because no drawImage.

Maybe add click to add seed only once but drag continuous. Pointermove threshold not needed; each move call seed.

We should ensure `new Uint32Array(img.data.buffer)` assignment after createImageData. Good.

Potential bug: `putImageData` outputs raw pixel array. Canvas CSS scales it. Fine.

Could use `devicePixelRatio`? Grid fixed. CSS handles.

Let's finalize code and write file.

Potential additional enhancements:
- Display current pattern name.
- Keyboard controls.

Info overlay update innerHTML with pattern name and fps. Keep simple. We'll update info.

Fps: measure every ~0.5s.

Implement:

const info = document.getElementById('info');
...
info.innerHTML = `...`

Add keyboard:
window.addEventListener('keydown', e=>{
  if(e.key==='1') p=params[0];
  else if(e.key==='2') ...
  else if(e.key==='r') reset();
  else if(e.key===' ') running=!running;
});

Declared p as params[0].

Could dynamic update info.

Now, final step: write index.html.

write /workspace/index.html (7712 bytes)
toolResult

Successfully wrote 7712 bytes to /workspace/index.html

assistant
thinking
 Now we should verify file exists, maybe run a quick static server? No network for external CDNs but we can open with a headless check? Could just verify syntax by node parse? Use `node --check` on HTML wouldn't work. Maybe use bash to verify file. We could also start python http server and curl but not necessary. At least read file maybe. Or run a quick Node script to extract JS and parse? Could simply run with `node -e` parse string? Better: use a small Node snippet to extract script contents and `new Function` or `Function` parse. But robust enough.

Let's read maybe first few lines to ensure content looks right. Actually nothing else needed. But we can maybe ensure not syntax errors. Use bash grep. Let's just verify with quick parse: read file and use node to extract script via regex and output to temp, then `node --check temp.js`. Let's do.

bash {"command":"node -e \"const fs=require('fs'); let h=fs.readFileSync('index.html','utf8'); let m=h.match(/<script>([\\s\\S]*)<\\/script>/); i
toolResult

js syntax ok

assistant
thinking
  Good. Now final concise response. Mention path, features, controls.

Created `/workspace/index.html` — a single-file Gray-Scott reaction-diffusion simulation. - Organic Turing patterns grow in real time on a canvas - Mouse / touch drag seeds new activator patches anywhere - Switchable regimes: coral, spots, mazes (keys 1-3) - R resets, Space pauses - No external resources, CDNs, or network calls — fully self-contained Open `index.html` in a browser to run it.

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

modelkimi-k2.7-code
vendorMoonshot · Kimi
harnesspi-agent
generated2026-07-10

EVALUATION

3/5 working

Gray-Scott reaction-diffusion pattern forms with small yellow blobs; drag/click seed visible growth, but scattered dots look uniform and static.

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.

idle motion0.6%
on click0.3%
on drag2.7%
on wheel1.0%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread26.7 / 255
console errors0
js errors none

METRICS

runtime118.1s
tokens in16k
tokens out8.1k
cached3.5k
cost / run$0.0405