The Grain of Before
Version 1 / JavaScript / research-quickjs-v1
Usage
Call simulate(input). Frame event_index 0 is the untouched surface; index n shows the state after event n. Presses within a sequence teach directed grain and deform the surface. Release clears deformation and ends that sequence while preserving grain. The next press probes the inherited direction. Wait relaxes deformation and fades grain; higher wait strength advances forgetting further. Optional parameters: retention in 0..1 (default 0.82), and learning in 0..2 (default 0.9). Compare the two final reliefs and deformation centroids. Setting learning to 0 provides an order-memory control: after release, their final probes should coincide. Field values are rounded integers; mechanism metrics retain six decimal places.
Source SHA-256: 57f3d6c297dab25f28789979ba75799be8a3cd86f90f449e3584c216af9be64d
Published source
function simulate(input) {
var W=24,H=18,N=W*H;
var gx=new Array(N).fill(0),gy=new Array(N).fill(0),load=new Array(N).fill(0),base=[];
var frames=[],last=null,transitions=0,forgotten=0,hash=2166136261;
var seed=input.seed;
for(var k=0;k<seed.length;k++) hash=Math.imul(hash^seed.charCodeAt(k),16777619)>>>0;
function bounded(v,a,b){return Math.max(a,Math.min(b,v));}
var p=input.parameters||{};
var retention=typeof p.retention==='number'?bounded(p.retention,0,1):0.82;
var learning=typeof p.learning==='number'?bounded(p.learning,0,2):0.9;
for(var i=0;i<N;i++) {
hash=(Math.imul(hash,1664525)+1013904223)>>>0;
var bx=(i%W)/(W-1),by=Math.floor(i/W)/(H-1);
base.push(75+16*Math.sin(bx*9+by*5)+12*(hash/4294967296-0.5));
}
function energy(){var s=0;for(var j=0;j<N;j++)s+=Math.sqrt(gx[j]*gx[j]+gy[j]*gy[j]);return s/N;}
function snapshot(index,label){
var values=[];
for(var j=0;j<N;j++){
var g=Math.sqrt(gx[j]*gx[j]+gy[j]*gy[j]);
var height=base[j]+65*g/(1+g)+760*(1-Math.exp(-load[j]/760));
values.push(Math.round(bounded(height,0,1000)));
}
frames.push({event_index:index,label:label,visual:{kind:'field',width:W,height:H,values:values,palette:['#081923','#235166','#52AFA8','#E1D8A0','#FFF3DB'],description:'Height and color encode resting substrate plus residual grain seam and current deformation. A press creates a central ridge and a stronger displaced ridge along remembered grain.'}});
}
snapshot(0,'Untaught surface');
for(var e=0;e<input.events.length;e++){
var ev=input.events[e],x=ev.x/1000,y=ev.y/1000,s=ev.strength/1000;
if(ev.kind==='release'){
load.fill(0);last=null;
}else if(ev.kind==='wait'){
var before=energy(),decay=Math.pow(retention,1+3*s);
for(var j=0;j<N;j++){gx[j]*=decay;gy[j]*=decay;load[j]*=0.45;}
forgotten+=before-energy();
}else{
if(last!==null){
var dx=x-last.x,dy=y-last.y,len=Math.sqrt(dx*dx+dy*dy);
if(len>0.000001){
var ux=dx/len,uy=dy/len,amount=learning*Math.sqrt(s*last.s);
for(var j=0;j<N;j++){
var px=(j%W)/(W-1),py=Math.floor(j/W)/(H-1);
var t=bounded(((px-last.x)*dx+(py-last.y)*dy)/(len*len),0,1);
var qx=px-last.x-t*dx,qy=py-last.y-t*dy;
var a=amount*Math.exp(-(qx*qx+qy*qy)/(2*0.105*0.105));
gx[j]+=a*ux;gy[j]+=a*uy;
}
transitions++;
}
}
for(var j=0;j<N;j++)load[j]*=0.35;
for(var j=0;j<N;j++){
var px=(j%W)/(W-1),py=Math.floor(j/W)/(H-1);
var rx=px-x,ry=py-y;
var deposit=560*s*Math.exp(-(rx*rx+ry*ry)/(2*0.095*0.095));
var g=Math.sqrt(gx[j]*gx[j]+gy[j]*gy[j]);
var split=g/(0.28+g),shift=0.19*split;
var tx=px+(g>0?shift*gx[j]/g:0),ty=py+(g>0?shift*gy[j]/g:0);
load[j]+=deposit*(1-0.72*split);
var fx=bounded(tx,0,1)*(W-1),fy=bounded(ty,0,1)*(H-1);
var ix=Math.floor(fx),iy=Math.floor(fy),ax=fx-ix,ay=fy-iy;
var ix1=Math.min(ix+1,W-1),iy1=Math.min(iy+1,H-1),m=deposit*0.72*split;
load[iy*W+ix]+=m*(1-ax)*(1-ay);
load[iy*W+ix1]+=m*ax*(1-ay);
load[iy1*W+ix]+=m*(1-ax)*ay;
load[iy1*W+ix1]+=m*ax*ay;
}
last={x:x,y:y,s:s};
}
snapshot(e+1,ev.kind==='release'?'Lift: grain remains':ev.kind==='wait'?'Wait: grain fades':'Press: inherited direction');
}
var mass=0,cx=0,cy=0,mx=0,my=0;
for(var j=0;j<N;j++){
mass+=load[j];cx+=load[j]*(j%W)/(W-1);cy+=load[j]*Math.floor(j/W)/(H-1);mx+=gx[j];my+=gy[j];
}
function rounded(v){return Math.round(v*1000000)/1000000;}
return {frames:frames,metrics:{directed_transitions:transitions,mean_grain_magnitude:rounded(energy()),mean_grain_x:rounded(mx/N),mean_grain_y:rounded(my/N),forgotten_grain:rounded(forgotten),deformation_mass:rounded(mass),deformation_centroid_x:rounded(mass?1000*cx/mass:500),deformation_centroid_y:rounded(mass?1000*cy/mass:500)}};
}