Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /><title>flow — paint a current, color rides it</title><style> :root{ --glass:rgba(14,16,28,.82); --line:rgba(255,255,255,.10); --ink:#e6ebf6; --dim:#8b95b0; --pink:#ff5b8a; --gold:#ffd23c; } *{ box-sizing:border-box; } html,body{ margin:0; height:100%; overflow:hidden; background:#05060d; color:var(--ink); font:13px/1.45 ui-sans-serif,system-ui,sans-serif; } #v{ position:fixed; left:0; top:0; image-rendering:pixelated; cursor:crosshair; touch-action:none; background:#05060d; } .ui{ position:fixed; background:var(--glass); border:1px solid var(--line); border-radius:14px; backdrop-filter:blur(9px); -webkit-backdrop-filter:blur(9px); box-shadow:0 8px 40px rgba(0,0,0,.5); } #dock{ top:14px; left:14px; width:230px; padding:12px 13px; } #dock.collapsed .body{ display:none; } .hd{ display:flex; justify-content:space-between; align-items:center; cursor:pointer; user-select:none; } .hd b span{ color:var(--pink); } .hd .chev{ color:var(--dim); } .body{ margin-top:10px; display:flex; flex-direction:column; gap:11px; } .grp{ display:flex; flex-direction:column; gap:6px; } .lab{ font-size:10px; letter-spacing:.6px; text-transform:uppercase; color:var(--dim); } .seg{ display:flex; flex-wrap:wrap; gap:5px; } .seg button{ flex:1 1 auto; background:rgba(255,255,255,.05); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:6px 8px; cursor:pointer; font:inherit; font-size:12px; white-space:nowrap; } .seg button:hover{ border-color:var(--pink); } .seg button[aria-pressed=true]{ background:var(--pink); color:#1a0510; border-color:var(--pink); font-weight:600; } .row{ display:flex; align-items:center; gap:9px; font-size:11px; color:var(--dim); } .row input[type=range]{ flex:1; accent-color:var(--pink); height:3px; } .row .val{ color:var(--gold); min-width:30px; text-align:right; font-variant-numeric:tabular-nums; } .sw{ display:flex; gap:5px; flex-wrap:wrap; } .sw button{ width:22px; height:22px; border-radius:6px; border:1px solid var(--line); cursor:pointer; padding:0; } .sw button[aria-pressed=true]{ outline:2px solid #fff; outline-offset:1px; } .actions{ display:flex; gap:6px; } .actions button{ background:rgba(255,255,255,.05); border:1px solid var(--line); color:var(--ink); border-radius:8px; padding:6px 10px; cursor:pointer; font:inherit; font-size:12px; } .actions button:hover{ border-color:var(--pink); } .hide{ display:none!important; } #tip{ bottom:14px; left:14px; padding:8px 12px; color:var(--dim); font-size:11px; } #tip b{ color:var(--ink); }</style></head><body><canvas id="v"></canvas>
<div id="dock" class="ui"> <div class="hd" id="hd"><b>paint a <span>current</span></b><span class="chev">▾</span></div> <div class="body"> <div class="grp"> <div class="lab">tool</div> <div class="seg" id="tools"> <button data-t="flow" aria-pressed="true">flow</button> <button data-t="pigment">pigment</button> <button data-t="sort">sort</button> <button data-t="erase">erase</button> </div> </div> <div class="grp"> <div class="lab">brush</div> <div class="seg" id="shapes"> <button data-s="circle" aria-pressed="true">● circle</button> <button data-s="rect">▮ rect</button> </div> <div class="row">size <input id="sz" type="range" min="3" max="90" value="26"><span class="val" id="szV">26</span></div> </div>
<div class="grp hide" id="pigGrp"> <div class="lab">pigment</div> <div class="sw" id="sw"></div> </div>
<div class="grp hide" id="sortGrp"> <div class="lab">sort</div> <div class="seg" id="algo"> <button data-a="bubble" aria-pressed="true">bubble</button> <button data-a="insertion">insertion</button> <button data-a="selection">selection</button> </div> <div class="seg" id="key"> <button data-k="bright" aria-pressed="true">bright</button> <button data-k="hue">hue</button> <button data-k="red">red</button> </div> <div class="seg" id="dir"> <button data-d="1" aria-pressed="true">↑</button> <button data-d="-1">↓</button> </div> <div class="row">speed <input id="ss" type="range" min="1" max="80" value="14"><span class="val" id="ssV">14</span></div> </div>
<div class="grp"> <div class="lab">field</div> <div class="row">flow <input id="fl" type="range" min="0" max="40" value="14"><span class="val" id="flV">1.4</span></div> <div class="row">diffuse <input id="df" type="range" min="0" max="40" value="6"><span class="val" id="dfV">.06</span></div> </div>
<div class="grp"> <div class="actions"> <button id="seed">seed</button> <button id="clear">clear</button> <button id="pause">pause</button> </div> </div> </div></div>
<div id="tip" class="ui"><b>drag</b> to push the current · color streams along it · <b>pigment</b> injects, <b>sort</b> orders a region</div>
<script>const v=document.getElementById('v'), ctx=v.getContext('2d');
// --- square pixels filling the viewport --------------------------------------const CELL=Math.max(2, Math.round(window.innerHeight/180));const GW=Math.ceil(window.innerWidth/CELL), GH=Math.ceil(window.innerHeight/CELL), N=GW*GH;v.width=GW; v.height=GH; v.style.width=(GW*CELL)+'px'; v.style.height=(GH*CELL)+'px';const img=ctx.createImageData(GW,GH), px=img.data;
// --- the ONE field: colour + a painted flow vector + a sort flag -------------let r=new Float32Array(N), g=new Float32Array(N), b=new Float32Array(N);let nr=new Float32Array(N), ng=new Float32Array(N), nb=new Float32Array(N);const fx=new Float32Array(N), fy=new Float32Array(N); // painted current (persistent)const flag=new Uint8Array(N);
function seed(){ for(let y=0;y<GH;y++)for(let x=0;x<GW;x++){ const i=y*GW+x; r[i]=0.5+0.45*Math.sin(x*0.05+y*0.012); g[i]=0.5+0.45*Math.sin(y*0.06+2.1); b[i]=0.5+0.45*Math.sin((x+y)*0.035+4.2); }}function clearAll(){ r.fill(0);g.fill(0);b.fill(0);fx.fill(0);fy.fill(0);flag.fill(0); sortOn=false; }seed();
// --- bilinear sample with wrap ----------------------------------------------function samp(buf,X,Y){ X=((X%GW)+GW)%GW; Y=((Y%GH)+GH)%GH; const x0=Math.floor(X), y0=Math.floor(Y), x1=(x0+1)%GW, y1=(y0+1)%GH, tx=X-x0, ty=Y-y0; const a=buf[y0*GW+x0], c=buf[y0*GW+x1], d=buf[y1*GW+x0], e=buf[y1*GW+x1]; return a*(1-tx)*(1-ty)+c*tx*(1-ty)+d*(1-tx)*ty+e*tx*ty;}
// --- step: colour rides the current (semi-Lagrangian advection) + diffuse ----let flow=1.4, diffuse=0.06, paused=false;function step(){ for(let y=0;y<GH;y++)for(let x=0;x<GW;x++){ const i=y*GW+x; if(flag[i]){ nr[i]=r[i]; ng[i]=g[i]; nb[i]=b[i]; continue; } // sorted pixels are frozen const bx=x-fx[i]*flow, by=y-fy[i]*flow; // trace back upstream let R=samp(r,bx,by), G=samp(g,bx,by), B=samp(b,bx,by); if(diffuse>0){ const xl=(x+GW-1)%GW, xr=(x+1)%GW, yu=(y+GH-1)%GH, yd=(y+1)%GH; const aR=(r[y*GW+xl]+r[y*GW+xr]+r[yu*GW+x]+r[yd*GW+x])*0.25; const aG=(g[y*GW+xl]+g[y*GW+xr]+g[yu*GW+x]+g[yd*GW+x])*0.25; const aB=(b[y*GW+xl]+b[y*GW+xr]+b[yu*GW+x]+b[yd*GW+x])*0.25; R=R*(1-diffuse)+aR*diffuse; G=G*(1-diffuse)+aG*diffuse; B=B*(1-diffuse)+aB*diffuse; } nr[i]=R; ng[i]=G; nb[i]=B; } let t=r;r=nr;nr=t; t=g;g=ng;ng=t; t=b;b=nb;nb=t; if(sortOn) sortPass();}
// --- classic sorts within flagged horizontal runs (one micro-step / call) ----const SS={ s:new Int32Array(GH).fill(-1), e:new Int32Array(GH), i:new Int32Array(GH), j:new Int32Array(GH), m:new Int32Array(GH), end:new Int32Array(GH), done:new Uint8Array(GH) };function keyAt(x,key){ const R=r[x],G=g[x],B=b[x]; if(key==='red') return R; if(key==='hue'){ const mx=Math.max(R,G,B),mn=Math.min(R,G,B),dd=mx-mn||1e-6; let h; if(mx===R)h=((G-B)/dd)%6; else if(mx===G)h=(B-R)/dd+2; else h=(R-G)/dd+4; return (h+6)%6; } return 0.299*R+0.587*G+0.114*B; }function swap(a,c){ let t=r[a];r[a]=r[c];r[c]=t; t=g[a];g[a]=g[c];g[c]=t; t=b[a];b[a]=b[c];b[c]=t; }let sortOn=false, algo='bubble', skey='bright', sdir=1, ssteps=14;function sortPass(){ for(let y=0;y<GH;y++){ let s=-1,e=0; const base=y*GW; for(let x=0;x<GW;x++){ if(flag[base+x]){ if(s<0)s=x; e=x+1; } } if(s<0){ SS.s[y]=-1; continue; } s+=base; e+=base; if(SS.s[y]!==s||SS.e[y]!==e){ SS.s[y]=s;SS.e[y]=e;SS.done[y]=0; SS.i[y]=algo==='insertion'?s+1:s; SS.j[y]=s+1; SS.m[y]=s; SS.end[y]=e; } for(let k=0;k<ssteps && !SS.done[y]; k++){ if(algo==='insertion'){ let i=SS.i[y],j=SS.j[y]; if(i>=e){SS.done[y]=1;break;} if(j>s && (keyAt(j-1,skey)-keyAt(j,skey))*sdir>0){ swap(j-1,j); j--; } else { i++; j=i; } SS.i[y]=i; SS.j[y]=j; } else if(algo==='selection'){ let i=SS.i[y],j=SS.j[y],m=SS.m[y]; if(i>=e-1){SS.done[y]=1;break;} if(j<e){ if((keyAt(j,skey)-keyAt(m,skey))*sdir<0) m=j; j++; } else { swap(i,m); i++; m=i; j=i+1; } SS.i[y]=i; SS.j[y]=j; SS.m[y]=m; } else { let i=SS.i[y],end=SS.end[y]; if(end<=s+1){SS.done[y]=1;break;} if((keyAt(i,skey)-keyAt(i+1,skey))*sdir>0) swap(i,i+1); i++; if(i>=end-1){ i=s; end--; } SS.i[y]=i; SS.end[y]=end; } } }}
// --- render -----------------------------------------------------------------function render(){ for(let i=0;i<N;i++){ const o=i*4; px[o]=Math.min(255,Math.max(0,r[i]*255)); px[o+1]=Math.min(255,Math.max(0,g[i]*255)); px[o+2]=Math.min(255,Math.max(0,b[i]*255)); px[o+3]=255; } ctx.putImageData(img,0,0);}function loop(){ if(!paused) step(); render(); requestAnimationFrame(loop); }requestAnimationFrame(loop);
// --- painting ---------------------------------------------------------------let tool='flow', shape='circle', size=26, color=[1,0.3,0.5], down=false, lastX=0,lastY=0;const SWATCHES=[[1,.18,.42],[1,.6,.1],[1,.85,.2],[.3,.9,.45],[.2,.7,1],[.6,.35,1],[1,1,1],[0,0,0]];function cell(ev){ const rc=v.getBoundingClientRect(); return { x:(ev.clientX-rc.left)/rc.width*GW, y:(ev.clientY-rc.top)/rc.height*GH }; }function stamp(cx,cy,vx,vy){ const rad=size/2, r2=rad*rad; for(let dy=-rad;dy<=rad;dy++)for(let dx=-rad;dx<=rad;dx++){ if(shape==='circle'){ if(dx*dx+dy*dy>r2) continue; } else { if(Math.abs(dx)>rad||Math.abs(dy)>rad) continue; } const x=(cx+dx)|0, y=(cy+dy)|0; if(x<0||y<0||x>=GW||y>=GH) continue; const i=y*GW+x; if(tool==='flow'){ fx[i]=vx; fy[i]=vy; } else if(tool==='pigment'){ r[i]=color[0]; g[i]=color[1]; b[i]=color[2]; } else if(tool==='sort'){ flag[i]=1; } else { flag[i]=0; fx[i]=0; fy[i]=0; } }}v.addEventListener('pointerdown',e=>{ down=true; v.setPointerCapture(e.pointerId); const c=cell(e); lastX=c.x; lastY=c.y; stamp(c.x,c.y,0,0); if(tool==='sort'){ sortOn=true; } });v.addEventListener('pointermove',e=>{ if(!down) return; const c=cell(e); let vx=c.x-lastX, vy=c.y-lastY; const m=Math.hypot(vx,vy)||1; vx/=m; vy/=m; // drag direction → current stamp(c.x,c.y,vx,vy); lastX=c.x; lastY=c.y; });v.addEventListener('pointerup',()=>down=false);
// --- UI ---------------------------------------------------------------------function seg(id,attr,fn){ const el=document.getElementById(id); el.querySelectorAll('button').forEach(btn=>btn.onclick=()=>{ el.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed','false')); btn.setAttribute('aria-pressed','true'); fn(btn.dataset[attr]); }); }seg('tools','t',x=>{ tool=x; document.getElementById('pigGrp').classList.toggle('hide',x!=='pigment'); document.getElementById('sortGrp').classList.toggle('hide',x!=='sort'); });seg('shapes','s',x=>shape=x);seg('algo','a',x=>algo=x); seg('key','k',x=>skey=x); seg('dir','d',x=>sdir=+x);function sl(id,fn,fmt){ const el=document.getElementById(id),o=document.getElementById(id+'V'); el.oninput=()=>{ o.textContent=fmt(fn(+el.value)); }; }sl('sz',x=>size=x,x=>x);sl('ss',x=>ssteps=x,x=>x);sl('fl',x=>flow=x/10,x=>x.toFixed(1));sl('df',x=>diffuse=x/100,x=>x.toFixed(2).slice(1));const sw=document.getElementById('sw');SWATCHES.forEach((c,i)=>{ const btn=document.createElement('button'); btn.style.background=`rgb(${c[0]*255|0},${c[1]*255|0},${c[2]*255|0})`; btn.setAttribute('aria-pressed',i===0); btn.onclick=()=>{ sw.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed','false')); btn.setAttribute('aria-pressed','true'); color=c; }; sw.appendChild(btn); });color=SWATCHES[0];document.getElementById('seed').onclick=seed;document.getElementById('clear').onclick=clearAll;document.getElementById('pause').onclick=e=>{ paused=!paused; e.target.textContent=paused?'play':'pause'; };document.getElementById('hd').onclick=()=>document.getElementById('dock').classList.toggle('collapsed');</script></body></html>