Upload index.html
Browse files- index.html +55 -0
index.html
CHANGED
|
@@ -139,6 +139,7 @@ const ABS_DUR=1.6,BUD_DUR=2.6; // merge/split transit
|
|
| 139 |
function step(dt){
|
| 140 |
const midY=(LIQ_TOP+LIQ_BOT)/2;
|
| 141 |
for(const b of blobs){
|
|
|
|
| 142 |
if(b.childOf)continue; // absorbed child is driven below
|
| 143 |
const t=(b.y-LIQ_TOP)/(LIQ_BOT-LIQ_TOP);
|
| 144 |
const er=b.r*WAXR; // world radius at current wax amount
|
|
@@ -203,6 +204,7 @@ function step(dt){
|
|
| 203 |
u.child.r=u.r2*lerp(0.15,1,k);
|
| 204 |
if(u.t>=1){ // free now - keep them from re-fusing mid-drift
|
| 205 |
u.child.budding=false;u.child.partner=b;b.partner=u.child;b.bud=null;
|
|
|
|
| 206 |
}
|
| 207 |
}
|
| 208 |
|
|
@@ -210,6 +212,10 @@ function step(dt){
|
|
| 210 |
for(let i=0;i<blobs.length;i++)for(let j=i+1;j<blobs.length;j++){
|
| 211 |
const a=blobs[i],c=blobs[j];
|
| 212 |
if(a.abs||a.childOf||c.abs||c.childOf||a.bud||c.bud||a.budding||c.budding)continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
if((a.partner&&a.partner===c)||(c.partner&&c.partner===a))continue;
|
| 214 |
const dx=c.x-a.x,dy=c.y-a.y,d=Math.hypot(dx,dy)||0.001,rs=a.r+c.r;
|
| 215 |
if(d<rs*0.32){
|
|
@@ -256,6 +262,18 @@ function step(dt){
|
|
| 256 |
const child={x:nx,y:ny,vx:b.vx+Math.cos(ang)*(10+Math.random()*8),vy:-5-Math.random()*6,r:r2*0.15,temp:Math.min(1,b.temp+0.05),seed:Math.random()*1000,budding:true};
|
| 257 |
blobs.push(child);
|
| 258 |
b.bud={child,t:0,dur:0.9,rFrom:b.r,rTo:Math.max(18,Math.cbrt(b.r**3-r2**3)),r2};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
}
|
| 260 |
}
|
| 261 |
}
|
|
@@ -437,6 +455,42 @@ function drawSlider(){
|
|
| 437 |
ctx.fillStyle=g;ctx.beginPath();ctx.arc(kx,SL.y,r+1.5,0,Math.PI*2);ctx.fill();
|
| 438 |
ctx.restore();
|
| 439 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
function drawLiquid(){
|
| 441 |
ctx.save();ctx.clip(liquidClip);
|
| 442 |
const g=ctx.createLinearGradient(0,LIQ_TOP,0,LIQ_BOT);
|
|
@@ -552,6 +606,7 @@ function frame(now){
|
|
| 552 |
drawGlass();
|
| 553 |
drawBase();
|
| 554 |
drawSlider();
|
|
|
|
| 555 |
canvas.style.cursor=(slDrag||slHover)?'ew-resize':(ledHover?'pointer':'default');
|
| 556 |
}catch(err){
|
| 557 |
const d=document.createElement('div');
|
|
|
|
| 139 |
function step(dt){
|
| 140 |
const midY=(LIQ_TOP+LIQ_BOT)/2;
|
| 141 |
for(const b of blobs){
|
| 142 |
+
if(b.splitAge!==undefined)b.splitAge+=dt;
|
| 143 |
if(b.childOf)continue; // absorbed child is driven below
|
| 144 |
const t=(b.y-LIQ_TOP)/(LIQ_BOT-LIQ_TOP);
|
| 145 |
const er=b.r*WAXR; // world radius at current wax amount
|
|
|
|
| 204 |
u.child.r=u.r2*lerp(0.15,1,k);
|
| 205 |
if(u.t>=1){ // free now - keep them from re-fusing mid-drift
|
| 206 |
u.child.budding=false;u.child.partner=b;b.partner=u.child;b.bud=null;
|
| 207 |
+
u.child.splitAge=0; // start the grace period timer
|
| 208 |
}
|
| 209 |
}
|
| 210 |
|
|
|
|
| 212 |
for(let i=0;i<blobs.length;i++)for(let j=i+1;j<blobs.length;j++){
|
| 213 |
const a=blobs[i],c=blobs[j];
|
| 214 |
if(a.abs||a.childOf||c.abs||c.childOf||a.bud||c.bud||a.budding||c.budding)continue;
|
| 215 |
+
// protect newly split-off blobs from immediate absorption (3s grace period)
|
| 216 |
+
if((a.splitAge!==undefined&&a.splitAge<3)||(c.splitAge!==undefined&&c.splitAge<3))continue;
|
| 217 |
+
// don't absorb very small blobs (they're still establishing themselves)
|
| 218 |
+
if(a.r<12||c.r<12)continue;
|
| 219 |
if((a.partner&&a.partner===c)||(c.partner&&c.partner===a))continue;
|
| 220 |
const dx=c.x-a.x,dy=c.y-a.y,d=Math.hypot(dx,dy)||0.001,rs=a.r+c.r;
|
| 221 |
if(d<rs*0.32){
|
|
|
|
| 262 |
const child={x:nx,y:ny,vx:b.vx+Math.cos(ang)*(10+Math.random()*8),vy:-5-Math.random()*6,r:r2*0.15,temp:Math.min(1,b.temp+0.05),seed:Math.random()*1000,budding:true};
|
| 263 |
blobs.push(child);
|
| 264 |
b.bud={child,t:0,dur:0.9,rFrom:b.r,rTo:Math.max(18,Math.cbrt(b.r**3-r2**3)),r2};
|
| 265 |
+
// burst of carbonated bubbles at the boil location
|
| 266 |
+
const count=4+Math.floor(Math.random()*5); // 4-8 bubbles for a bigger burst
|
| 267 |
+
for(let i=0;i<count;i++){
|
| 268 |
+
blobBubs.push({
|
| 269 |
+
x:nx+(Math.random()-0.5)*6,
|
| 270 |
+
y:ny+(Math.random()-0.5)*6,
|
| 271 |
+
vy:-(20+Math.random()*18),
|
| 272 |
+
r:0.7+Math.random()*1.8,
|
| 273 |
+
ph:Math.random()*7,
|
| 274 |
+
wob:2+Math.random()*2
|
| 275 |
+
});
|
| 276 |
+
}
|
| 277 |
}
|
| 278 |
}
|
| 279 |
}
|
|
|
|
| 455 |
ctx.fillStyle=g;ctx.beginPath();ctx.arc(kx,SL.y,r+1.5,0,Math.PI*2);ctx.fill();
|
| 456 |
ctx.restore();
|
| 457 |
}
|
| 458 |
+
|
| 459 |
+
// film grain overlay - subtle animated noise for that analog feel (larger grains)
|
| 460 |
+
function drawGrain(){
|
| 461 |
+
const cw=canvas.width,ch=canvas.height;
|
| 462 |
+
const imgData=ctx.getImageData(0,0,cw,ch);
|
| 463 |
+
const data=imgData.data;
|
| 464 |
+
const grainSize=3*DPR; // scale grain size with DPR
|
| 465 |
+
// precompute blob positions in canvas coordinates for wax detection
|
| 466 |
+
const ox=CX-MAX_HW-pad,oy=LIQ_TOP-pad;
|
| 467 |
+
const baseIntensity=2.25; // very subtle grain intensity (reduced 25% more)
|
| 468 |
+
const waxMultiplier=2.25; // 125% stronger where wax is (another 50% on top)
|
| 469 |
+
for(let y=0;y<ch;y+=grainSize){
|
| 470 |
+
for(let x=0;x<cw;x+=grainSize){
|
| 471 |
+
// check if this grain cell overlaps any blob (wax mask)
|
| 472 |
+
let onWax=false;
|
| 473 |
+
const bufX=CX-MAX_HW-pad,bufY=LIQ_TOP-pad; // where wax buffer is drawn
|
| 474 |
+
const wx=(x/DPR)-bufX,wy=(y/DPR)-bufY; // convert to world coords
|
| 475 |
+
for(const b of blobs){
|
| 476 |
+
const bx=b.x-ox,by=b.y-oy;
|
| 477 |
+
const rr=b.r*WAXR*1.12;
|
| 478 |
+
if(Math.hypot(wx-bx,wy-by)<rr){onWax=true;break;}
|
| 479 |
+
}
|
| 480 |
+
const intensity=onWax?baseIntensity*waxMultiplier:baseIntensity;
|
| 481 |
+
const n=(Math.random()-0.5)*intensity;
|
| 482 |
+
for(let gy=0;gy<grainSize&&y+gy<ch;gy++){
|
| 483 |
+
for(let gx=0;gx<grainSize&&x+gx<cw;gx++){
|
| 484 |
+
const idx=((Math.floor(y+gy)*cw+Math.floor(x+gx))*4);
|
| 485 |
+
data[idx]=Math.max(0,Math.min(255,data[idx]+n)); // R
|
| 486 |
+
data[idx+1]=Math.max(0,Math.min(255,data[idx+1]+n)); // G
|
| 487 |
+
data[idx+2]=Math.max(0,Math.min(255,data[idx+2]+n)); // B
|
| 488 |
+
}
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
ctx.putImageData(imgData,0,0);
|
| 493 |
+
}
|
| 494 |
function drawLiquid(){
|
| 495 |
ctx.save();ctx.clip(liquidClip);
|
| 496 |
const g=ctx.createLinearGradient(0,LIQ_TOP,0,LIQ_BOT);
|
|
|
|
| 606 |
drawGlass();
|
| 607 |
drawBase();
|
| 608 |
drawSlider();
|
| 609 |
+
drawGrain();
|
| 610 |
canvas.style.cursor=(slDrag||slHover)?'ew-resize':(ledHover?'pointer':'default');
|
| 611 |
}catch(err){
|
| 612 |
const d=document.createElement('div');
|