One commited on
Commit
7159a02
·
verified ·
1 Parent(s): 9e32425

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +56 -20
index.html CHANGED
@@ -92,12 +92,31 @@ function setWaxFromX(px){
92
  }
93
  function overSlider(p){return Math.abs(p.x-SL.x)<SL.w/2+16&&Math.abs(p.y-SL.y)<18;}
94
  function canvasXY(e){const r=canvas.getBoundingClientRect();return{x:(e.clientX-r.left)*W/r.width,y:(e.clientY-r.top)*H/r.height};}
95
- canvas.addEventListener('pointerdown',e=>{const p=canvasXY(e);if(overSlider(p)){slDrag=true;try{canvas.setPointerCapture(e.pointerId);}catch(_){}setWaxFromX(p.x);e.preventDefault();}});
96
- canvas.addEventListener('pointermove',e=>{const p=canvasXY(e);slHover=overSlider(p);if(slDrag)setWaxFromX(p.x);});
97
  const endSlDrag=e=>{if(slDrag){slDrag=false;try{canvas.releasePointerCapture(e.pointerId);}catch(_){}}};
98
  canvas.addEventListener('pointerup',endSlDrag);
99
  canvas.addEventListener('pointercancel',endSlDrag);
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  // ---- physics (px units) ----
102
  let time=0,last=performance.now();
103
  const SIM_SPEED=0.55; // slow, heavy wax (scales all motion + transitions)
@@ -115,7 +134,7 @@ function step(dt){
115
  // heater at the bottom: strongest right on the base, so even big blobs
116
  // resting on their own floor get fully heated; cooler air up top
117
  if(b.y>botY-38) b.temp+=dt*0.5*(b.y-(botY-38))/38;
118
- else b.temp-=dt*(0.045+(t<0.3?0.10:0));
119
  b.temp=Math.max(0,Math.min(1,b.temp));
120
  // buoyancy: screen y grows downward, so negative ay = rising.
121
  // hot = light (rises), cold = heavy (sinks); bigger blobs weigh more
@@ -256,8 +275,8 @@ function renderBlobs(){
256
  g=(8+Math.pow(k,1.8)*150)*(0.55+t*0.6);
257
  bl=(34-Math.pow(k,2)*14+k*k*70)*(0.6+t*0.5);
258
  // white-hot core for very hot, deep wax (glowing from within)
259
- if(f>1.4&&t>0.6){
260
- const w=Math.min(1,(f-1.4)/0.8)*Math.min(1,(t-0.6)/0.35)*0.7;
261
  r+=(255-r)*w;g+=(225-g)*w;bl+=(190-bl)*w;
262
  }
263
  a=255*Math.min(1,(f-0.52)/0.35);
@@ -265,6 +284,11 @@ function renderBlobs(){
265
  const k=(f-0.3)/0.22;
266
  r=255;g=60*k;bl=40*k;a=110*k*k;
267
  }
 
 
 
 
 
268
  d[i*4]=Math.min(255,r);d[i*4+1]=Math.min(255,g);d[i*4+2]=Math.min(255,bl);d[i*4+3]=a;
269
  }
270
  bctx.putImageData(img,0,0);
@@ -279,7 +303,7 @@ function drawBackground(){
279
  // warm light pool from the lamp onto the room
280
  ctx.save();ctx.globalCompositeOperation='lighter';
281
  const lg=ctx.createRadialGradient(CX,H-40,20,CX,H-40,H*0.62);
282
- lg.addColorStop(0,'rgba(255,70,35,0.16)');lg.addColorStop(0.5,'rgba(255,60,30,0.05)');lg.addColorStop(1,'rgba(255,60,30,0)');
283
  ctx.fillStyle=lg;ctx.fillRect(0,0,W,H);ctx.restore();
284
  ctx.save();ctx.globalAlpha=0.55;ctx.fillStyle='#000';
285
  ctx.beginPath();ctx.ellipse(CX,H-24,190,26,0,0,Math.PI*2);ctx.fill();ctx.restore();
@@ -287,7 +311,7 @@ function drawBackground(){
287
  function drawLED(){
288
  ctx.save();ctx.globalCompositeOperation='lighter';
289
  const g=ctx.createRadialGradient(CX,H-58,4,CX,H-58,150);
290
- g.addColorStop(0,'rgba(255,60,35,0.38)');g.addColorStop(1,'rgba(255,60,35,0)');
291
  ctx.fillStyle=g;ctx.fillRect(CX-170,H-210,340,210);ctx.restore();
292
  }
293
  function drawCap(){
@@ -318,8 +342,11 @@ function drawBase(){
318
  rim.addColorStop(0,'#6a5590');rim.addColorStop(1,'#171026');
319
  ctx.fillStyle=rim;ctx.beginPath();ctx.roundRect(CX-MIN_HW-16,645,(MIN_HW+16)*2,16,8);ctx.fill();
320
  const pulse=0.75+0.25*Math.sin(time*2);
321
- ctx.save();ctx.shadowColor='rgba(255,70,40,0.9)';ctx.shadowBlur=12*pulse;
322
- ctx.fillStyle=`rgba(255,80,45,${0.85*pulse})`;ctx.beginPath();ctx.arc(CX,H-52,3.4,0,Math.PI*2);ctx.fill();ctx.restore();
 
 
 
323
  }
324
  function drawSlider(){
325
  const kx=SL.x-SL.w/2+slF*SL.w;
@@ -331,12 +358,12 @@ function drawSlider(){
331
  ctx.beginPath();ctx.roundRect(SL.x-SL.w/2+0.5,SL.y-1.3,SL.w-1,2.6,1.3);ctx.stroke();
332
  // knob, echoing the LED above it
333
  const r=(slDrag||slHover)?5:4;
334
- ctx.shadowColor='rgba(255,80,40,0.9)';
335
  ctx.shadowBlur=slDrag?15:(slHover?11:7);
336
  const g=ctx.createRadialGradient(kx,SL.y,0.5,kx,SL.y,r+1.5);
337
- g.addColorStop(0,'rgba(255,238,220,0.95)');
338
- g.addColorStop(0.45,'rgba(255,130,70,0.85)');
339
- g.addColorStop(1,'rgba(255,70,35,0)');
340
  ctx.fillStyle=g;ctx.beginPath();ctx.arc(kx,SL.y,r+1.5,0,Math.PI*2);ctx.fill();
341
  ctx.restore();
342
  }
@@ -349,14 +376,14 @@ function drawLiquid(){
349
  // ---- bottom lamp glow: heat rising up the liquid column ----
350
  ctx.globalCompositeOperation='lighter';
351
  const hg=ctx.createLinearGradient(0,LIQ_BOT-190,0,LIQ_BOT+10);
352
- hg.addColorStop(0,'rgba(255,80,45,0)');
353
- hg.addColorStop(0.6,'rgba(255,70,40,0.10)');
354
- hg.addColorStop(1,'rgba(255,110,60,0.38)');
355
  ctx.fillStyle=hg;ctx.fillRect(CX-MAX_HW-8,LIQ_BOT-190,MAX_HW*2+16,200);
356
  const led=ctx.createRadialGradient(CX,LIQ_BOT+6,4,CX,LIQ_BOT+6,150);
357
- led.addColorStop(0,'rgba(255,150,95,0.55)');
358
- led.addColorStop(0.35,'rgba(255,85,50,0.24)');
359
- led.addColorStop(1,'rgba(255,60,40,0)');
360
  ctx.fillStyle=led;ctx.fillRect(CX-170,LIQ_BOT-150,340,170);
361
 
362
  // drifting shimmer
@@ -401,6 +428,15 @@ function frame(now){
401
  try{
402
  const dt=Math.max(0,Math.min(0.05,(now-last)/1000));last=now;time+=dt;
403
  step(dt*SIM_SPEED);
 
 
 
 
 
 
 
 
 
404
  drawBackground();
405
  drawLED();
406
  drawCap();
@@ -408,7 +444,7 @@ function frame(now){
408
  drawGlass();
409
  drawBase();
410
  drawSlider();
411
- canvas.style.cursor=slDrag||slHover?'ew-resize':'default';
412
  }catch(err){
413
  const d=document.createElement('div');
414
  d.style.cssText='position:fixed;left:50%;top:8px;transform:translateX(-50%);color:#ffb3a7;background:rgba(20,5,10,.9);padding:8px 14px;border-radius:8px;font:13px monospace;z-index:9';
 
92
  }
93
  function overSlider(p){return Math.abs(p.x-SL.x)<SL.w/2+16&&Math.abs(p.y-SL.y)<18;}
94
  function canvasXY(e){const r=canvas.getBoundingClientRect();return{x:(e.clientX-r.left)*W/r.width,y:(e.clientY-r.top)*H/r.height};}
95
+ canvas.addEventListener('pointerdown',e=>{const p=canvasXY(e);if(overLED(p)){RGB_MODE=!RGB_MODE;e.preventDefault();return;}if(overSlider(p)){slDrag=true;try{canvas.setPointerCapture(e.pointerId);}catch(_){}setWaxFromX(p.x);e.preventDefault();}});
96
+ canvas.addEventListener('pointermove',e=>{const p=canvasXY(e);ledHover=overLED(p);slHover=overSlider(p)&&!ledHover;if(slDrag)setWaxFromX(p.x);});
97
  const endSlDrag=e=>{if(slDrag){slDrag=false;try{canvas.releasePointerCapture(e.pointerId);}catch(_){}}};
98
  canvas.addEventListener('pointerup',endSlDrag);
99
  canvas.addEventListener('pointercancel',endSlDrag);
100
 
101
+ // ---- clickable LED: toggles a slow RGB colour cycle on the whole lamp ----
102
+ let RGB_MODE=false,rgbMix=0,hue=140; // mix eases 0(default) <-> 1(full RGB)
103
+ const HUE_SPEED=360/16; // one full colour cycle every ~16 s
104
+ const DEF_L={r:255,g:90,b:45}; // the lamp's original warm colour
105
+ const LED_POS={x:CX,y:H-52};
106
+ let ledHover=false;
107
+ function overLED(p){return Math.hypot(p.x-LED_POS.x,p.y-LED_POS.y)<20;}
108
+ function hueRGB(h){ // pleasant saturated RGB from a hue (deg)
109
+ h=((h%360)+360)%360;const s=0.84,v=1,c=v*s,x=c*(1-Math.abs((h/60)%2-1)),m=v-c;
110
+ let r,g,b;
111
+ if(h<60){r=c;g=x;b=0;}else if(h<120){r=x;g=c;b=0;}
112
+ else if(h<180){r=0;g=c;b=x;}else if(h<240){r=0;g=x;b=c;}
113
+ else if(h<300){r=x;g=0;b=c;}else{r=c;g=0;b=x;}
114
+ return{r:Math.round((r+m)*255),g:Math.round((g+m)*255),b:Math.round((b+m)*255)};
115
+ }
116
+ const rgba=(c,a)=>`rgba(${c.r|0},${c.g|0},${c.b|0},${a})`;
117
+ 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});
118
+ let LIGHT={...DEF_L},LUM_L=1; // effective lamp colour, updated each frame
119
+
120
  // ---- physics (px units) ----
121
  let time=0,last=performance.now();
122
  const SIM_SPEED=0.55; // slow, heavy wax (scales all motion + transitions)
 
134
  // heater at the bottom: strongest right on the base, so even big blobs
135
  // resting on their own floor get fully heated; cooler air up top
136
  if(b.y>botY-38) b.temp+=dt*0.5*(b.y-(botY-38))/38;
137
+ else b.temp-=dt*(0.025+(t<0.3?0.06:0));
138
  b.temp=Math.max(0,Math.min(1,b.temp));
139
  // buoyancy: screen y grows downward, so negative ay = rising.
140
  // hot = light (rises), cold = heavy (sinks); bigger blobs weigh more
 
275
  g=(8+Math.pow(k,1.8)*150)*(0.55+t*0.6);
276
  bl=(34-Math.pow(k,2)*14+k*k*70)*(0.6+t*0.5);
277
  // white-hot core for very hot, deep wax (glowing from within)
278
+ if(f>1.4&&t>0.35){
279
+ const w=Math.min(1,(f-1.4)/0.8)*Math.min(1,(t-0.35)/0.5)*0.7;
280
  r+=(255-r)*w;g+=(225-g)*w;bl+=(190-bl)*w;
281
  }
282
  a=255*Math.min(1,(f-0.52)/0.35);
 
284
  const k=(f-0.3)/0.22;
285
  r=255;g=60*k;bl=40*k;a=110*k*k;
286
  }
287
+ if(rgbMix>0&&a>0){ // re-light the wax by the current lamp colour
288
+ const lum=0.299*r+0.587*g+0.114*bl;
289
+ if(lum>1){const s=Math.min(1.45,lum/LUM_L);
290
+ r+=(LIGHT.r*s-r)*rgbMix;g+=(LIGHT.g*s-g)*rgbMix;bl+=(LIGHT.b*s-bl)*rgbMix;}
291
+ }
292
  d[i*4]=Math.min(255,r);d[i*4+1]=Math.min(255,g);d[i*4+2]=Math.min(255,bl);d[i*4+3]=a;
293
  }
294
  bctx.putImageData(img,0,0);
 
303
  // warm light pool from the lamp onto the room
304
  ctx.save();ctx.globalCompositeOperation='lighter';
305
  const lg=ctx.createRadialGradient(CX,H-40,20,CX,H-40,H*0.62);
306
+ lg.addColorStop(0,rgba(LIGHT,0.16));lg.addColorStop(0.5,rgba(LIGHT,0.05));lg.addColorStop(1,rgba(LIGHT,0));
307
  ctx.fillStyle=lg;ctx.fillRect(0,0,W,H);ctx.restore();
308
  ctx.save();ctx.globalAlpha=0.55;ctx.fillStyle='#000';
309
  ctx.beginPath();ctx.ellipse(CX,H-24,190,26,0,0,Math.PI*2);ctx.fill();ctx.restore();
 
311
  function drawLED(){
312
  ctx.save();ctx.globalCompositeOperation='lighter';
313
  const g=ctx.createRadialGradient(CX,H-58,4,CX,H-58,150);
314
+ g.addColorStop(0,rgba(LIGHT,0.38));g.addColorStop(1,rgba(LIGHT,0));
315
  ctx.fillStyle=g;ctx.fillRect(CX-170,H-210,340,210);ctx.restore();
316
  }
317
  function drawCap(){
 
342
  rim.addColorStop(0,'#6a5590');rim.addColorStop(1,'#171026');
343
  ctx.fillStyle=rim;ctx.beginPath();ctx.roundRect(CX-MIN_HW-16,645,(MIN_HW+16)*2,16,8);ctx.fill();
344
  const pulse=0.75+0.25*Math.sin(time*2);
345
+ const ledR=ledHover?4.3:3.4;
346
+ ctx.save();ctx.shadowColor=rgba(LIGHT,0.9);ctx.shadowBlur=12*pulse*(ledHover?1.6:1);
347
+ ctx.fillStyle=rgba(LIGHT,Math.min(1,0.85*pulse*(ledHover?1.3:1)));ctx.beginPath();ctx.arc(CX,H-52,ledR,0,Math.PI*2);ctx.fill();
348
+ ctx.fillStyle=rgba(mixWhite(LIGHT,0.6),0.9*pulse*(ledHover?1.15:1));ctx.beginPath();ctx.arc(CX,H-52,ledR*0.45,0,Math.PI*2);ctx.fill();
349
+ ctx.restore();
350
  }
351
  function drawSlider(){
352
  const kx=SL.x-SL.w/2+slF*SL.w;
 
358
  ctx.beginPath();ctx.roundRect(SL.x-SL.w/2+0.5,SL.y-1.3,SL.w-1,2.6,1.3);ctx.stroke();
359
  // knob, echoing the LED above it
360
  const r=(slDrag||slHover)?5:4;
361
+ ctx.shadowColor=rgba(LIGHT,0.9);
362
  ctx.shadowBlur=slDrag?15:(slHover?11:7);
363
  const g=ctx.createRadialGradient(kx,SL.y,0.5,kx,SL.y,r+1.5);
364
+ g.addColorStop(0,'rgba(255,240,225,0.95)');
365
+ g.addColorStop(0.45,rgba(LIGHT,0.85));
366
+ g.addColorStop(1,rgba(LIGHT,0));
367
  ctx.fillStyle=g;ctx.beginPath();ctx.arc(kx,SL.y,r+1.5,0,Math.PI*2);ctx.fill();
368
  ctx.restore();
369
  }
 
376
  // ---- bottom lamp glow: heat rising up the liquid column ----
377
  ctx.globalCompositeOperation='lighter';
378
  const hg=ctx.createLinearGradient(0,LIQ_BOT-190,0,LIQ_BOT+10);
379
+ hg.addColorStop(0,rgba(LIGHT,0));
380
+ hg.addColorStop(0.6,rgba(LIGHT,0.10));
381
+ hg.addColorStop(1,rgba(LIGHT,0.38));
382
  ctx.fillStyle=hg;ctx.fillRect(CX-MAX_HW-8,LIQ_BOT-190,MAX_HW*2+16,200);
383
  const led=ctx.createRadialGradient(CX,LIQ_BOT+6,4,CX,LIQ_BOT+6,150);
384
+ led.addColorStop(0,rgba(mixWhite(LIGHT,0.4),0.55));
385
+ led.addColorStop(0.35,rgba(LIGHT,0.24));
386
+ led.addColorStop(1,rgba(LIGHT,0));
387
  ctx.fillStyle=led;ctx.fillRect(CX-170,LIQ_BOT-150,340,170);
388
 
389
  // drifting shimmer
 
428
  try{
429
  const dt=Math.max(0,Math.min(0.05,(now-last)/1000));last=now;time+=dt;
430
  step(dt*SIM_SPEED);
431
+ // ---- lamp colour: default warm, or slow RGB cycle when the LED is clicked ----
432
+ hue=(hue+dt*HUE_SPEED)%360;
433
+ const tm=RGB_MODE?1:0;
434
+ rgbMix+=(tm-rgbMix)*(1-Math.exp(-dt*2.8));
435
+ if(!RGB_MODE&&rgbMix<0.004)rgbMix=0;
436
+ if(RGB_MODE&&rgbMix>0.996)rgbMix=1;
437
+ const hc=hueRGB(hue);
438
+ LIGHT={r:DEF_L.r+(hc.r-DEF_L.r)*rgbMix,g:DEF_L.g+(hc.g-DEF_L.g)*rgbMix,b:DEF_L.b+(hc.b-DEF_L.b)*rgbMix};
439
+ LUM_L=Math.max(1,0.299*LIGHT.r+0.587*LIGHT.g+0.114*LIGHT.b);
440
  drawBackground();
441
  drawLED();
442
  drawCap();
 
444
  drawGlass();
445
  drawBase();
446
  drawSlider();
447
+ canvas.style.cursor=(slDrag||slHover)?'ew-resize':(ledHover?'pointer':'default');
448
  }catch(err){
449
  const d=document.createElement('div');
450
  d.style.cssText='position:fixed;left:50%;top:8px;transform:translateX(-50%);color:#ffb3a7;background:rgba(20,5,10,.9);padding:8px 14px;border-radius:8px;font:13px monospace;z-index:9';