Upload index.html
Browse files- index.html +63 -12
index.html
CHANGED
|
@@ -81,6 +81,8 @@ for(let i=0;i<6;i++){
|
|
| 81 |
|
| 82 |
// ---- bubbles (tiny, for life) ----
|
| 83 |
const bubbles=[];for(let i=0;i<14;i++)bubbles.push({x:Math.random(),y:Math.random(),s:0.5+Math.random(),r:0.8+Math.random()*1.6,ph:Math.random()*7});
|
|
|
|
|
|
|
| 84 |
|
| 85 |
// ---- wax amount (unlabeled slider on the base, just under the LED) ----
|
| 86 |
// volume multiplier 0.25x..4x; middle = as-is. Radius scales as cbrt(volume).
|
|
@@ -123,6 +125,8 @@ function hueRGB(h){ // pleasant saturated RGB from a hue
|
|
| 123 |
}
|
| 124 |
const rgba=(c,a)=>`rgba(${c.r|0},${c.g|0},${c.b|0},${a})`;
|
| 125 |
const mixWhite=(c,t)=>({r:c.r+(255-c.r)*t,g:c.g+(255-c.g)*t,b:c.b+(255-c.b)*t});
|
|
|
|
|
|
|
| 126 |
let LIGHT={...DEF_L},LUM_L=1; // effective lamp colour, updated each frame
|
| 127 |
|
| 128 |
// ---- physics (px units) ----
|
|
@@ -263,6 +267,38 @@ function step(dt){
|
|
| 263 |
}
|
| 264 |
|
| 265 |
for(const b of bubbles){b.y-=dt*(0.03+0.02*b.s);if(b.y<0){b.y=1;b.x=Math.random();}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
// ---- metaball rendering (gooey, temperature-tinted glow) ----
|
|
@@ -313,10 +349,11 @@ function renderBlobs(){
|
|
| 313 |
const k=(f-0.3)/0.22;
|
| 314 |
r=255;g=60*k;bl=40*k;a=110*k*k;
|
| 315 |
}
|
| 316 |
-
if(rgbMix>0&&a>0){ // re-light the wax by the current lamp colour
|
| 317 |
const lum=0.299*r+0.587*g+0.114*bl;
|
| 318 |
if(lum>1){const s=Math.min(1.45,lum/LUM_L);
|
| 319 |
-
|
|
|
|
| 320 |
}
|
| 321 |
// soft tone map to prevent overexposure blips (like a camera sensor flattening)
|
| 322 |
const tone=v=>v<=230?v:230+25*(1-Math.exp(-(v-230)/8));
|
|
@@ -331,18 +368,20 @@ function drawBackground(){
|
|
| 331 |
const g=ctx.createRadialGradient(CX,H*0.42,60,CX,H*0.42,H*0.75);
|
| 332 |
g.addColorStop(0,'#181026');g.addColorStop(1,'#05030a');
|
| 333 |
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
|
| 334 |
-
// warm light pool from the lamp onto the room
|
|
|
|
| 335 |
ctx.save();ctx.globalCompositeOperation='lighter';
|
| 336 |
const lg=ctx.createRadialGradient(CX,H-40,20,CX,H-40,H*0.62);
|
| 337 |
-
lg.addColorStop(0,rgba(LIGHT,0.16));lg.addColorStop(0.5,rgba(LIGHT,0.05));lg.addColorStop(1,rgba(LIGHT,0));
|
| 338 |
ctx.fillStyle=lg;ctx.fillRect(0,0,W,H);ctx.restore();
|
| 339 |
ctx.save();ctx.globalAlpha=0.55;ctx.fillStyle='#000';
|
| 340 |
ctx.beginPath();ctx.ellipse(CX,H-24,190,26,0,0,Math.PI*2);ctx.fill();ctx.restore();
|
| 341 |
}
|
| 342 |
function drawLED(){
|
|
|
|
| 343 |
ctx.save();ctx.globalCompositeOperation='lighter';
|
| 344 |
const g=ctx.createRadialGradient(CX,H-58,4,CX,H-58,150);
|
| 345 |
-
g.addColorStop(0,rgba(LIGHT,0.38));g.addColorStop(1,rgba(LIGHT,0));
|
| 346 |
ctx.fillStyle=g;ctx.fillRect(CX-170,H-210,340,210);ctx.restore();
|
| 347 |
}
|
| 348 |
function drawCap(){
|
|
@@ -401,26 +440,31 @@ function drawSlider(){
|
|
| 401 |
function drawLiquid(){
|
| 402 |
ctx.save();ctx.clip(liquidClip);
|
| 403 |
const g=ctx.createLinearGradient(0,LIQ_TOP,0,LIQ_BOT);
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
| 405 |
ctx.fillStyle=g;ctx.fillRect(CX-MAX_HW-8,LIQ_TOP-8,MAX_HW*2+16,LIQ_BOT-LIQ_TOP+16);
|
| 406 |
|
| 407 |
-
// ---- bottom lamp glow: heat rising up the liquid column ----
|
|
|
|
| 408 |
ctx.globalCompositeOperation='lighter';
|
| 409 |
const hg=ctx.createLinearGradient(0,LIQ_BOT-190,0,LIQ_BOT+10);
|
| 410 |
hg.addColorStop(0,rgba(LIGHT,0));
|
| 411 |
-
hg.addColorStop(0.6,rgba(LIGHT,0.
|
| 412 |
-
hg.addColorStop(1,rgba(LIGHT,0.
|
| 413 |
ctx.fillStyle=hg;ctx.fillRect(CX-MAX_HW-8,LIQ_BOT-190,MAX_HW*2+16,200);
|
| 414 |
const led=ctx.createRadialGradient(CX,LIQ_BOT+6,4,CX,LIQ_BOT+6,150);
|
| 415 |
-
led.addColorStop(0,rgba(mixWhite(LIGHT,0.4),0.
|
| 416 |
-
led.addColorStop(0.35,rgba(LIGHT,0.
|
| 417 |
led.addColorStop(1,rgba(LIGHT,0));
|
| 418 |
ctx.fillStyle=led;ctx.fillRect(CX-170,LIQ_BOT-150,340,170);
|
| 419 |
|
| 420 |
// drifting shimmer
|
| 421 |
const sx=CX+Math.sin(time*0.4)*60;
|
| 422 |
const sh=ctx.createRadialGradient(sx,(LIQ_TOP+LIQ_BOT)/2,10,sx,(LIQ_TOP+LIQ_BOT)/2,220);
|
| 423 |
-
|
|
|
|
| 424 |
ctx.fillStyle=sh;ctx.fillRect(CX-MAX_HW-8,LIQ_TOP-8,MAX_HW*2+16,LIQ_BOT-LIQ_TOP+16);
|
| 425 |
|
| 426 |
// bubbles
|
|
@@ -432,6 +476,13 @@ function drawLiquid(){
|
|
| 432 |
ctx.beginPath();ctx.arc(x,y,b.r,0,Math.PI*2);ctx.fill();
|
| 433 |
}
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
// ---- the lava: diffuse halo + glowing body ----
|
| 436 |
renderBlobs();
|
| 437 |
const bx=CX-MAX_HW-pad,by=LIQ_TOP-pad;
|
|
|
|
| 81 |
|
| 82 |
// ---- bubbles (tiny, for life) ----
|
| 83 |
const bubbles=[];for(let i=0;i<14;i++)bubbles.push({x:Math.random(),y:Math.random(),s:0.5+Math.random(),r:0.8+Math.random()*1.6,ph:Math.random()*7});
|
| 84 |
+
// carbonation: air bubbles shed from the blobs' surfaces (purely cosmetic)
|
| 85 |
+
const blobBubs=[];
|
| 86 |
|
| 87 |
// ---- wax amount (unlabeled slider on the base, just under the LED) ----
|
| 88 |
// volume multiplier 0.25x..4x; middle = as-is. Radius scales as cbrt(volume).
|
|
|
|
| 125 |
}
|
| 126 |
const rgba=(c,a)=>`rgba(${c.r|0},${c.g|0},${c.b|0},${a})`;
|
| 127 |
const mixWhite=(c,t)=>({r:c.r+(255-c.r)*t,g:c.g+(255-c.g)*t,b:c.b+(255-c.b)*t});
|
| 128 |
+
// Blend a fixed colour toward the lamp light's hue as RGB mode fades in (rgbMix 0->1)
|
| 129 |
+
const tint=(orig)=>{const t=0.25*rgbMix;return{r:orig.r*(1-t)+LIGHT.r*t,g:orig.g*(1-t)+LIGHT.g*t,b:orig.b*(1-t)+LIGHT.b*t};};
|
| 130 |
let LIGHT={...DEF_L},LUM_L=1; // effective lamp colour, updated each frame
|
| 131 |
|
| 132 |
// ---- physics (px units) ----
|
|
|
|
| 267 |
}
|
| 268 |
|
| 269 |
for(const b of bubbles){b.y-=dt*(0.03+0.02*b.s);if(b.y<0){b.y=1;b.x=Math.random();}}
|
| 270 |
+
|
| 271 |
+
// blobs fizz: hotter wax sheds tiny air bubbles off its surface, like
|
| 272 |
+
// carbonation coming out of solution (no interaction with the blobs)
|
| 273 |
+
for(const b of blobs){
|
| 274 |
+
if(Math.random()<dt*(0.08+b.temp*0.3)){
|
| 275 |
+
const rr=b.r*WAXR;
|
| 276 |
+
const ang=Math.random()*Math.PI*2;
|
| 277 |
+
let x=b.x+Math.cos(ang)*rr*(1.02+Math.random()*0.1);
|
| 278 |
+
const y=b.y+Math.sin(ang)*rr*(1.02+Math.random()*0.1);
|
| 279 |
+
const hw=halfWidth(y)-4; // keep inside the glass
|
| 280 |
+
if(Math.abs(x-CX)>hw)x=CX+(x>CX?hw:-hw);
|
| 281 |
+
// spawn a small group of bubbles (like an air pocket bursting)
|
| 282 |
+
const count=2+Math.floor(Math.random()*4); // 2-5 bubbles
|
| 283 |
+
for(let i=0;i<count;i++){
|
| 284 |
+
blobBubs.push({
|
| 285 |
+
x:x+(Math.random()-0.5)*3,
|
| 286 |
+
y:y+(Math.random()-0.5)*3,
|
| 287 |
+
vy:-(16+Math.random()*14),
|
| 288 |
+
r:0.7+Math.random()*1.5,
|
| 289 |
+
ph:Math.random()*7,
|
| 290 |
+
wob:2+Math.random()*2
|
| 291 |
+
});
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
for(let i=blobBubs.length-1;i>=0;i--){
|
| 296 |
+
const q=blobBubs[i];
|
| 297 |
+
q.vy-=dt*6; // accelerate upward as it rises
|
| 298 |
+
q.y+=q.vy*dt;
|
| 299 |
+
q.x+=Math.sin(time*3+q.ph)*q.wob*dt; // gentle sideways wiggle
|
| 300 |
+
if(q.y<LIQ_TOP+4||blobBubs.length>120)blobBubs.splice(i,1);
|
| 301 |
+
}
|
| 302 |
}
|
| 303 |
|
| 304 |
// ---- metaball rendering (gooey, temperature-tinted glow) ----
|
|
|
|
| 349 |
const k=(f-0.3)/0.22;
|
| 350 |
r=255;g=60*k;bl=40*k;a=110*k*k;
|
| 351 |
}
|
| 352 |
+
if(rgbMix>0&&a>0){ // re-light the wax by the current lamp colour (50% blend)
|
| 353 |
const lum=0.299*r+0.587*g+0.114*bl;
|
| 354 |
if(lum>1){const s=Math.min(1.45,lum/LUM_L);
|
| 355 |
+
const waxBlend=0.25*rgbMix; // only blend 25% toward RGB (halved)
|
| 356 |
+
r+=(LIGHT.r*s-r)*waxBlend;g+=(LIGHT.g*s-g)*waxBlend;bl+=(LIGHT.b*s-bl)*waxBlend;}
|
| 357 |
}
|
| 358 |
// soft tone map to prevent overexposure blips (like a camera sensor flattening)
|
| 359 |
const tone=v=>v<=230?v:230+25*(1-Math.exp(-(v-230)/8));
|
|
|
|
| 368 |
const g=ctx.createRadialGradient(CX,H*0.42,60,CX,H*0.42,H*0.75);
|
| 369 |
g.addColorStop(0,'#181026');g.addColorStop(1,'#05030a');
|
| 370 |
ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
|
| 371 |
+
// warm light pool from the lamp onto the room (scaled down in RGB mode)
|
| 372 |
+
const glowScale=1-rgbMix*0.85; // reduce additive glows by 85% in RGB mode (halved effect)
|
| 373 |
ctx.save();ctx.globalCompositeOperation='lighter';
|
| 374 |
const lg=ctx.createRadialGradient(CX,H-40,20,CX,H-40,H*0.62);
|
| 375 |
+
lg.addColorStop(0,rgba(LIGHT,0.16*glowScale));lg.addColorStop(0.5,rgba(LIGHT,0.05*glowScale));lg.addColorStop(1,rgba(LIGHT,0));
|
| 376 |
ctx.fillStyle=lg;ctx.fillRect(0,0,W,H);ctx.restore();
|
| 377 |
ctx.save();ctx.globalAlpha=0.55;ctx.fillStyle='#000';
|
| 378 |
ctx.beginPath();ctx.ellipse(CX,H-24,190,26,0,0,Math.PI*2);ctx.fill();ctx.restore();
|
| 379 |
}
|
| 380 |
function drawLED(){
|
| 381 |
+
const glowScale=1-rgbMix*0.85; // reduce additive glows by 85% in RGB mode (halved effect)
|
| 382 |
ctx.save();ctx.globalCompositeOperation='lighter';
|
| 383 |
const g=ctx.createRadialGradient(CX,H-58,4,CX,H-58,150);
|
| 384 |
+
g.addColorStop(0,rgba(LIGHT,0.38*glowScale));g.addColorStop(1,rgba(LIGHT,0));
|
| 385 |
ctx.fillStyle=g;ctx.fillRect(CX-170,H-210,340,210);ctx.restore();
|
| 386 |
}
|
| 387 |
function drawCap(){
|
|
|
|
| 440 |
function drawLiquid(){
|
| 441 |
ctx.save();ctx.clip(liquidClip);
|
| 442 |
const g=ctx.createLinearGradient(0,LIQ_TOP,0,LIQ_BOT);
|
| 443 |
+
// base liquid follows the lamp light's hue in RGB mode; stays purple by default (almost transparent)
|
| 444 |
+
g.addColorStop(0,rgba(tint({r:35,g:9,b:74}),0.15));
|
| 445 |
+
g.addColorStop(0.5,rgba(tint({r:27,g:6,b:64}),0.15));
|
| 446 |
+
g.addColorStop(1,rgba(tint({r:18,g:3,b:44}),0.15));
|
| 447 |
ctx.fillStyle=g;ctx.fillRect(CX-MAX_HW-8,LIQ_TOP-8,MAX_HW*2+16,LIQ_BOT-LIQ_TOP+16);
|
| 448 |
|
| 449 |
+
// ---- bottom lamp glow: heat rising up the liquid column (scaled for subtler RGB) ----
|
| 450 |
+
const glowScale=1-rgbMix*0.85; // reduce additive glows by 85% in RGB mode (halved effect)
|
| 451 |
ctx.globalCompositeOperation='lighter';
|
| 452 |
const hg=ctx.createLinearGradient(0,LIQ_BOT-190,0,LIQ_BOT+10);
|
| 453 |
hg.addColorStop(0,rgba(LIGHT,0));
|
| 454 |
+
hg.addColorStop(0.6,rgba(LIGHT,0.05*glowScale));
|
| 455 |
+
hg.addColorStop(1,rgba(LIGHT,0.18*glowScale));
|
| 456 |
ctx.fillStyle=hg;ctx.fillRect(CX-MAX_HW-8,LIQ_BOT-190,MAX_HW*2+16,200);
|
| 457 |
const led=ctx.createRadialGradient(CX,LIQ_BOT+6,4,CX,LIQ_BOT+6,150);
|
| 458 |
+
led.addColorStop(0,rgba(mixWhite(LIGHT,0.4),0.28*glowScale));
|
| 459 |
+
led.addColorStop(0.35,rgba(LIGHT,0.12*glowScale));
|
| 460 |
led.addColorStop(1,rgba(LIGHT,0));
|
| 461 |
ctx.fillStyle=led;ctx.fillRect(CX-170,LIQ_BOT-150,340,170);
|
| 462 |
|
| 463 |
// drifting shimmer
|
| 464 |
const sx=CX+Math.sin(time*0.4)*60;
|
| 465 |
const sh=ctx.createRadialGradient(sx,(LIQ_TOP+LIQ_BOT)/2,10,sx,(LIQ_TOP+LIQ_BOT)/2,220);
|
| 466 |
+
const shim=tint({r:150,g:90,b:220});
|
| 467 |
+
sh.addColorStop(0,rgba(shim,0.10));sh.addColorStop(1,rgba(shim,0));
|
| 468 |
ctx.fillStyle=sh;ctx.fillRect(CX-MAX_HW-8,LIQ_TOP-8,MAX_HW*2+16,LIQ_BOT-LIQ_TOP+16);
|
| 469 |
|
| 470 |
// bubbles
|
|
|
|
| 476 |
ctx.beginPath();ctx.arc(x,y,b.r,0,Math.PI*2);ctx.fill();
|
| 477 |
}
|
| 478 |
|
| 479 |
+
// carbonation bubbles shed by the blobs (same look, fade so they "pop")
|
| 480 |
+
for(const q of blobBubs){
|
| 481 |
+
ctx.globalAlpha=Math.min(1,(q.y-LIQ_TOP)/30);
|
| 482 |
+
ctx.beginPath();ctx.arc(q.x,q.y,q.r,0,Math.PI*2);ctx.fill();
|
| 483 |
+
}
|
| 484 |
+
ctx.globalAlpha=1;
|
| 485 |
+
|
| 486 |
// ---- the lava: diffuse halo + glowing body ----
|
| 487 |
renderBlobs();
|
| 488 |
const bx=CX-MAX_HW-pad,by=LIQ_TOP-pad;
|