aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/panel-widget-gallery.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
-rw-r--r--docs/prototypes/panel-widget-gallery.html247
1 files changed, 8 insertions, 239 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 6bbe3d9..0065645 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -1778,22 +1778,22 @@ card(M,'R13','Edgewise strip meter',
(st,rd)=>GW.edgeMeter(st,{value:.62,onChange:(v,t)=>rd(t)}),
'<b>a needle on a vertical scale.</b> The parchment strip reads attenuation down a compressed log scale; the bar rides its edge. Drag up/down; readout shows dB. After a channel-strip gain-reduction meter.');
card(M,'R43','Attitude indicator',
- `<svg id="attind" class="rsvg" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ (st,rd)=>GW.attitudeIndicator(st,{onChange:(v,t)=>rd(t)}),
'<b>orientation, not a number.</b> The horizon rolls and shifts behind the fixed amber airplane — two axes in one read. Drag it: left/right banks, up/down pitches. The first two-axis instrument in the kit. After a cockpit artificial horizon.');
card(M,'R44','Heading bug + servo needle',
- `<svg id="hdgbug" class="rsvg" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ (st,rd)=>GW.headingBug(st,{value:90,onChange:(v,t)=>rd(t)}),
'<b>commanded versus actual on one dial.</b> Drag to park the amber bug where you want to go; the needle chases it with honest servo lag. The gap between them IS the information. After an HSI heading bug and synchro repeaters.');
card(M,'R54','Vertical tape instrument',
- `<svg id="vtape" class="rsvg" viewBox="0 0 70 130" width="70" height="130"></svg>`,
+ (st,rd)=>GW.verticalTape(st,{value:24,onChange:(v,t)=>rd(t)}),
'<b>the scale moves, the index does not.</b> A scrolling tape behind a fixed amber index — your value is always at eye level and the neighborhood above and below stays visible. Drag to spin the tape. After the Ki-57 remote tachometer; modern cockpits tape everything.');
card(M,'R55','Twin-needle gauge',
- `<svg id="twinng" class="rsvg" viewBox="0 0 120 110" width="120" height="110"></svg>`,
+ (st,rd)=>GW.twinNeedle(st,{fuel:2.4,oil:3.1,onChange:(v,t)=>rd(t)}),
'<b>two values, one housing, mirrored scales.</b> Fuel presses up the left arc, oil up the right, each with its own needle from the shared hub. Drag either half to set that side. Not the crossed-needle (N13) — these are read separately, not at their intersection. After the Ki-57 fuel/oil pressure gauge.');
card(M,'R56','Comfort-zone crossed needles',
- `<svg id="comfort" class="rsvg" viewBox="0 0 130 122" width="130" height="122"></svg>`,
+ (st,rd)=>GW.comfortMeter(st,{temp:72,humidity:45,onChange:(v,t)=>rd(t)}),
'<b>two values, one verdict.</b> Temperature and humidity needles cross over a face printed with judgments — the crossing point lands in TOO WARM, TOO DRY, or JUST RIGHT. N13 derives a number from the crossing; this one derives an opinion. Drag each half. After a brass weather-station comfort meter.');
card(M,'R53','Circular chart recorder',
- `<svg id="circhart" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ (st,rd)=>GW.chartRecorder(st,{onChange:(v,t)=>rd(t)}),
'<b>cyclic time on round paper.</b> The pen sweeps a full day per revolution, so "same time yesterday" is the same angle and daily rhythm draws itself as a flower. Click for fresh paper. The strip chart shows a window; this shows the cycle. After a Partlow round-chart recorder.');
card(M,'R17','Round CRT scope',
(st,rd)=>GW.roundCrt(st,{onChange:(v,t)=>rd(t)}),
@@ -2380,76 +2380,7 @@ function setDdcal(d,w){ddDate=d;ddDay=w;const HAND=135;
setRd('R36',lit+' / 64 lit');});}
setRd('R36',lit+' / 64 lit');})();
-/* R43 attitude indicator: sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */
-(function(){const s=$('attind'),cx=65,cy=65;
- const defs=svgEl(s,'defs',{});
- const clip=svgEl(defs,'clipPath',{id:'aiClip'});
- svgEl(clip,'circle',{cx,cy,r:48});
- const sky=svgEl(defs,'linearGradient',{id:'aiSky',x1:0,y1:0,x2:0,y2:1});
- svgEl(sky,'stop',{offset:'0','stop-color':'#54677d'});
- svgEl(sky,'stop',{offset:'1','stop-color':'#3a4c60'});
- const gnd=svgEl(defs,'linearGradient',{id:'aiGnd',x1:0,y1:0,x2:0,y2:1});
- svgEl(gnd,'stop',{offset:'0','stop-color':'#5c4630'});
- svgEl(gnd,'stop',{offset:'1','stop-color':'#3c2e1e'});
- svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2});
- const g=svgEl(s,'g',{'clip-path':'url(#aiClip)'});
- const ball=svgEl(g,'g',{});
- svgEl(ball,'rect',{x:cx-90,y:cy-180,width:180,height:180,fill:'url(#aiSky)'});
- svgEl(ball,'rect',{x:cx-90,y:cy,width:180,height:180,fill:'url(#aiGnd)'});
- svgEl(ball,'line',{x1:cx-90,y1:cy,x2:cx+90,y2:cy,stroke:'#f3e7c5','stroke-width':1.4});
- for(const p of [-20,-10,10,20]){const y=cy-p*1.6,w=p%20===0?16:10;
- svgEl(ball,'line',{x1:cx-w,y1:y,x2:cx+w,y2:y,stroke:'#f3e7c5','stroke-width':.8,opacity:.85});
- svgEl(ball,'text',{x:cx+w+3,y:y+2,'font-size':4.6,'font-family':'var(--mono)',
- fill:'#f3e7c5',opacity:.85}).textContent=String(Math.abs(p));}
- // fixed bank scale + pointer + miniature aircraft
- for(const b of [-60,-45,-30,-20,-10,0,10,20,30,45,60]){
- const [x1,y1]=polar(cx,cy,47,b),[x2,y2]=polar(cx,cy,b%30===0?42:44,b);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':b===0?1.6:.9,opacity:.9});}
- svgEl(s,'polygon',{points:`${cx-3.5},${cy-38} ${cx+3.5},${cy-38} ${cx},${cy-44}`,fill:'var(--gold-hi)'});
- svgEl(s,'path',{d:`M ${cx-22} ${cy} L ${cx-7} ${cy} L ${cx-4} ${cy+4} M ${cx+22} ${cy} L ${cx+7} ${cy} L ${cx+4} ${cy+4}`,
- fill:'none',stroke:'var(--gold-hi)','stroke-width':2.4,'stroke-linecap':'round'});
- svgEl(s,'circle',{cx,cy,r:2,fill:'var(--gold-hi)'});
- let aiBank=0,aiPitch=0;
- const setAi=(b,p)=>{aiBank=Math.max(-60,Math.min(60,b));aiPitch=Math.max(-20,Math.min(20,p));
- ball.setAttribute('transform',`rotate(${(-aiBank).toFixed(1)},${cx},${cy}) translate(0,${(aiPitch*1.6).toFixed(1)})`);
- setRd('R43','BANK '+(aiBank<0?'L':aiBank>0?'R':'')+Math.abs(Math.round(aiBank))+' · PITCH '+(aiPitch>=0?'+':'-')+String(Math.abs(Math.round(aiPitch))).padStart(2,'0'));};
- s.style.touchAction='none';s.style.cursor='move';
- s.addEventListener('pointerdown',e=>{s.setPointerCapture(e.pointerId);
- const x0=e.clientX,y0=e.clientY,b0=aiBank,p0=aiPitch;
- const mv=ev=>setAi(b0+(ev.clientX-x0)*0.4,p0+(ev.clientY-y0)*0.25);
- const up=()=>{s.removeEventListener('pointermove',mv);s.removeEventListener('pointerup',up);s.removeEventListener('pointercancel',up);};
- s.addEventListener('pointermove',mv);s.addEventListener('pointerup',up);s.addEventListener('pointercancel',up);e.preventDefault();});
- setAi(0,0);})();
-
-/* R44 heading bug + servo needle: drag sets the command; the needle chases with lag */
-let hbCmd=90,hbAct=90,hbBug=null,hbNeedle=null;
-(function(){const s=$('hdgbug'),cx=65,cy=65;
- svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:50,fill:'#1a1714',stroke:'#2c2820','stroke-width':1});
- for(let d=0;d<360;d+=10){const maj=d%30===0;
- const [x1,y1]=polar(cx,cy,48,d),[x2,y2]=polar(cx,cy,maj?42:45,d);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':maj?1:.5,opacity:maj?.9:.5});}
- for(let d=0;d<360;d+=30){const lbl=d===0?'N':d===90?'E':d===180?'S':d===270?'W':String(d/10);
- const [x,y]=polar(cx,cy,35,d);
- svgEl(s,'text',{x,y:y+2.6,'text-anchor':'middle','font-size':d%90===0?8:6.4,'font-weight':700,
- 'font-family':'var(--mono)',fill:d%90===0?'var(--cream)':'var(--steel)'}).textContent=lbl;}
- svgEl(s,'line',{x1:cx,y1:cy-56,x2:cx,y2:cy-48,stroke:'var(--fail)','stroke-width':2});
- hbBug=svgEl(s,'path',{d:`M ${cx-6} ${cy-49} L ${cx-6} ${cy-44} L ${cx-2.5} ${cy-44} L ${cx} ${cy-47} L ${cx+2.5} ${cy-44} L ${cx+6} ${cy-44} L ${cx+6} ${cy-49} Z`,
- fill:'var(--gold-hi)',stroke:'#7d5c16','stroke-width':.7});
- hbNeedle=svgEl(s,'g',{});
- svgEl(hbNeedle,'polygon',{points:`${cx-2},${cy+10} ${cx+2},${cy+10} ${cx+1.2},${cy-40} ${cx},${cy-44} ${cx-1.2},${cy-40}`,
- fill:'#bfc4d0',stroke:'#4a4e58','stroke-width':.6});
- svgEl(s,'circle',{cx,cy,r:4.5,fill:'#3a3631',stroke:'#060505','stroke-width':1});
- const draw=()=>{hbBug.setAttribute('transform',`rotate(${hbCmd.toFixed(1)},${cx},${cy})`);
- hbNeedle.setAttribute('transform',`rotate(${hbAct.toFixed(1)},${cx},${cy})`);
- setRd('R44','CMD '+String(Math.round((hbCmd%360+360)%360)).padStart(3,'0')+' · ACT '+String(Math.round((hbAct%360+360)%360)).padStart(3,'0'));};
- dragDelta(s,()=>hbCmd,v=>{hbCmd=(v%360+360)%360;draw();},{min:-100000,max:100000,sens:1.2});
- draw();
- if(!matchMedia('(prefers-reduced-motion: reduce)').matches)
- setInterval(()=>{const diff=((hbCmd-hbAct+540)%360)-180;
- if(Math.abs(diff)<0.4){if(hbAct!==hbCmd){hbAct=hbCmd;draw();}return;}
- hbAct=(hbAct+diff*0.07+360)%360;draw();},80);
- else{hbAct=hbCmd;}})();
+/* R43, R44 extracted to widgets.js (GW.*) */
/* R45 flip-disc: bistable mechanical pixels; scaleX flip with color swap at the midpoint */
(function(){const s=$('flipdisc'),COLS=7,ROWS=5,STEP=14,X0=13,Y0=13;
@@ -2565,169 +2496,7 @@ let dkPos=0,dkCarry=0;
draw();tick();
if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tick,250);})();
-/* R53 circular chart recorder: a day per revolution; the pen draws the cycle */
-(function(){const s=$('circhart'),cx=65,cy=65;
- svgEl(s,'circle',{cx,cy,r:60,fill:'#17140f',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:54,fill:'#efe9da',stroke:'#8f897b','stroke-width':.8});
- for(const r of [20,31,42])
- svgEl(s,'circle',{cx,cy,r,fill:'none',stroke:'#b8ae98','stroke-width':.5});
- for(let h=0;h<24;h++){const a=h*15;
- const [x1,y1]=polar(cx,cy,54,a),[x2,y2]=polar(cx,cy,h%6===0?12:48,a);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'#b8ae98','stroke-width':h%6===0?.6:.35,opacity:.8});
- if(h%6===0){const [tx,ty]=polar(cx,cy,50,a+5);
- svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
- fill:'#6e6552'}).textContent=String(h).padStart(2,'0');}}
- const trace=svgEl(s,'path',{d:'',fill:'none',stroke:'#8f2416','stroke-width':1.1,'stroke-linejoin':'round'});
- const pen=svgEl(s,'line',{x1:cx,y1:cy,x2:cx,y2:cy-54,stroke:'#4a463e','stroke-width':1});
- const penDot=svgEl(s,'circle',{cx,cy:cy-30,r:2,fill:'#8f2416'});
- svgEl(s,'circle',{cx,cy,r:4,fill:'#4a463e'});
- const val=h=>52+30*Math.sin(h/24*2*Math.PI-2.1)+12*Math.sin(h/24*6*Math.PI)+4*Math.sin(h*2.7);
- const rOf=v=>12+Math.max(0,Math.min(100,v))*0.36;
- let pts=[],hour=0;
- const draw=()=>{const a=hour*15,r=rOf(val(hour));
- pts.push(polar(cx,cy,r,a));
- if(pts.length>1)trace.setAttribute('d','M '+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L '));
- pen.setAttribute('transform',`rotate(${a},${cx},${cy})`);
- const [px,py]=polar(cx,cy,r,a);
- penDot.setAttribute('cx',px);penDot.setAttribute('cy',py);
- setRd('R53',String(Math.floor(hour)).padStart(2,'0')+':'+(hour%1>=0.5?'30':'00')+' · '+Math.round(val(hour))+'%');};
- const reset=()=>{pts=[];hour=0;trace.setAttribute('d','');draw();};
- s.style.cursor='pointer';
- s.addEventListener('click',reset);
- if(matchMedia('(prefers-reduced-motion: reduce)').matches){
- for(hour=0;hour<24;hour+=0.25)draw();hour=23.75;}
- else{reset();setInterval(()=>{hour+=0.25;if(hour>=24){pts=[];hour=0;}draw();},375);}})();
-
-/* R54 vertical tape: the scale scrolls behind a fixed index; drag to drive the value */
-(function(){const s=$('vtape'),W=70,H=130,CX=38,CY=65;
- const MIN=5,MAX=35,PPU=5.5; // rpm x100, px per unit
- const defs=svgEl(s,'defs',{});
- const clip=svgEl(defs,'clipPath',{id:'vtClip'});
- svgEl(clip,'rect',{x:14,y:8,width:48,height:114,rx:4});
- svgEl(s,'rect',{x:2,y:2,width:66,height:126,rx:7,fill:'#1c1916',stroke:'#060505','stroke-width':1.5});
- svgEl(s,'rect',{x:14,y:8,width:48,height:114,rx:4,fill:'#0d0c0a',stroke:'#2c2820','stroke-width':1});
- const tapeG=svgEl(s,'g',{'clip-path':'url(#vtClip)'});
- const tape=svgEl(tapeG,'g',{});
- for(let u=MIN;u<=MAX;u++){const y=-u*PPU; // higher value further up the tape
- const maj=u%5===0;
- svgEl(tape,'line',{x1:maj?20:26,y1:y,x2:34,y2:y,stroke:'var(--cream)','stroke-width':maj?1.1:.55,opacity:maj?.95:.6});
- if(maj)svgEl(tape,'text',{x:48,y:y+3,'text-anchor':'middle','font-size':9,'font-weight':700,
- 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=String(u);}
- // fixed index: side pointer + line across the window at center height
- svgEl(s,'polygon',{points:`8,${CY} 15,${CY-4} 15,${CY+4}`,fill:'var(--gold-hi)'});
- svgEl(s,'line',{x1:15,y1:CY,x2:62,y2:CY,stroke:'var(--gold-hi)','stroke-width':1,opacity:.85});
- svgEl(s,'text',{x:CX,y:126,'text-anchor':'middle','font-size':5,'letter-spacing':'.1em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='RPM x100';
- let vt=24;
- const draw=()=>{tape.setAttribute('transform',`translate(0,${(CY+vt*PPU).toFixed(1)})`);
- setRd('R54','RPM '+Math.round(vt*100));};
- s.style.cursor='ns-resize';
- dragDelta(s,()=>vt,v=>{vt=Math.max(MIN,Math.min(MAX,v));draw();},{min:MIN,max:MAX,sens:(MAX-MIN)/140});
- draw();})();
-
-/* R55 twin-needle gauge: mirrored half-scales, one hub, two independent needles */
-(function(){const s=$('twinng'),cx=60,cy=62,R=44;
- svgEl(s,'circle',{cx,cy,r:52,fill:'#14110e',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:46,fill:'#1a1714',stroke:'#2c2820','stroke-width':1});
- // mirrored arcs: left = FUEL (0 at bottom-left up to 4 at top), right = OIL
- const angL=v=>-170+v/4*160; // 0..4 -> -170..-10 (left side, cw from top)
- const angR=v=>170-v/4*160; // 0..4 -> 170..10 (right side)
- for(let v=0;v<=4;v++){
- for(const [ang,anchor] of [[angL(v),'end'],[angR(v),'start']]){
- const [x1,y1]=polar(cx,cy,R,ang),[x2,y2]=polar(cx,cy,R-5,ang);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':1,opacity:.9});
- const [tx,ty]=polar(cx,cy,R-11,ang);
- svgEl(s,'text',{x:tx,y:ty+2.4,'text-anchor':'middle','font-size':6,'font-family':'var(--mono)',
- fill:'var(--cream)'}).textContent=String(v);}}
- for(let v=0.5;v<4;v+=0.5){if(v%1===0)continue;
- for(const ang of [angL(v),angR(v)]){
- const [x1,y1]=polar(cx,cy,R,ang),[x2,y2]=polar(cx,cy,R-3,ang);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':.5,opacity:.55});}}
- svgEl(s,'text',{x:cx-20,y:cy+34,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='FUEL';
- svgEl(s,'text',{x:cx+20,y:cy+34,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='OIL';
- svgEl(s,'text',{x:cx,y:cy-26,'text-anchor':'middle','font-size':4.6,'letter-spacing':'.06em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='kg/cm2';
- const needle=color=>{const g=svgEl(s,'g',{});
- svgEl(g,'polygon',{points:`${cx-1.6},${cy+8} ${cx+1.6},${cy+8} ${cx+1},${cy-R+8} ${cx},${cy-R+4} ${cx-1},${cy-R+8}`,
- fill:color,stroke:'rgba(0,0,0,.4)','stroke-width':.5});return g;};
- const nF=needle('#e0523a'),nO=needle('#bfc4d0');
- svgEl(s,'circle',{cx,cy,r:5,fill:'#3a3631',stroke:'#060505','stroke-width':1});
- let vF=2.4,vO=3.1;
- const draw=()=>{nF.setAttribute('transform',`rotate(${angL(vF).toFixed(1)},${cx},${cy})`);
- nO.setAttribute('transform',`rotate(${angR(vO).toFixed(1)},${cx},${cy})`);
- setRd('R55','FUEL '+vF.toFixed(1)+' · OIL '+vO.toFixed(1));};
- // each half is its own drag surface
- const half=(x,get,set)=>{const hit=svgEl(s,'rect',{x,y:10,width:60,height:104,fill:'transparent'});
- hit.style.cursor='ns-resize';
- dragDelta(hit,get,v=>{set(Math.max(0,Math.min(4,v)));draw();},{min:0,max:4,sens:4/110});};
- half(0,()=>vF,v=>vF=v);
- half(60,()=>vO,v=>vO=v);
- draw();})();
-
-/* R56 comfort meter: temp and humidity needles cross over printed zone verdicts */
-(function(){const s=$('comfort'),cx=65,cy=60;
- const defs=svgEl(s,'defs',{});
- const br=svgEl(defs,'linearGradient',{id:'cmBrass',x1:0,y1:0,x2:0,y2:1});
- svgEl(br,'stop',{offset:'0','stop-color':'#d8b25c'});
- svgEl(br,'stop',{offset:'.5','stop-color':'#a07c30'});
- svgEl(br,'stop',{offset:'1','stop-color':'#6e5218'});
- svgEl(s,'circle',{cx,cy,r:56,fill:'url(#cmBrass)',stroke:'#4a3610','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:53,fill:'none',stroke:'#7a5c1e','stroke-width':2.5,'stroke-dasharray':'2 1.4'});
- svgEl(s,'circle',{cx,cy,r:49,fill:'#efe9da',stroke:'#8f897b','stroke-width':1});
- const PT=[38,96],PH=[92,96]; // temp pivot (BL), humidity pivot (BR)
- const angT=t=>105-(t-40)/60*70; // 40..100F -> bearing 105..35 from PT
- const angH=h=>-105+h/100*70; // 0..100% -> bearing -105..-35 from PH
- // scale ticks along each sweep, near the rim
- for(let t=40;t<=100;t+=10){const a=angT(t);
- const [x1,y1]=polar(PT[0],PT[1],86,a),[x2,y2]=polar(PT[0],PT[1],82,a);
- if(Math.hypot(x1-cx,y1-cy)<48){svgEl(s,'line',{x1,y1,x2,y2,stroke:'#3c382f','stroke-width':t%20===0?1:.5});
- if(t%20===0){const [tx,ty]=polar(PT[0],PT[1],78,a);
- svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
- fill:'#3c382f'}).textContent=String(t);}}}
- for(let h=0;h<=100;h+=10){const a=angH(h);
- const [x1,y1]=polar(PH[0],PH[1],86,a),[x2,y2]=polar(PH[0],PH[1],82,a);
- if(Math.hypot(x1-cx,y1-cy)<48){svgEl(s,'line',{x1,y1,x2,y2,stroke:'#3c382f','stroke-width':h%20===0?1:.5});
- if(h%20===0){const [tx,ty]=polar(PH[0],PH[1],78,a);
- svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
- fill:'#3c382f'}).textContent=String(h);}}}
- // printed zone verdicts; the active one goes red
- const zones={};
- const zone=(key,txt,x,y,rot,size)=>{zones[key]=svgEl(s,'text',{x,y,'text-anchor':'middle',
- 'font-size':size||5.2,'font-weight':700,'letter-spacing':'.06em','font-family':'var(--mono)',
- fill:'#8f897b',transform:`rotate(${rot},${x},${y})`});zones[key].textContent=txt;};
- zone('humid','TOO HUMID',42,34,-32);
- zone('warm','TOO WARM',89,34,32);
- zone('right','JUST RIGHT',65,58,0,6.2);
- zone('dry','TOO DRY',38,74,28);
- zone('cold','TOO COLD',93,74,-28);
- svgEl(s,'text',{x:cx,y:92,'text-anchor':'middle','font-size':5.6,'letter-spacing':'.18em',
- 'font-family':'var(--mono)',fill:'#3c382f'}).textContent='WEATHER STATION';
- // needles: temp from bottom-left, humidity from bottom-right — clipped to the face
- const nclip=svgEl(defs,'clipPath',{id:'cmClip'});
- svgEl(nclip,'circle',{cx,cy,r:49});
- const ng=svgEl(s,'g',{'clip-path':'url(#cmClip)'});
- const needle=()=>{const g=svgEl(ng,'g',{});
- svgEl(g,'line',{x1:0,y1:0,x2:0,y2:-84,stroke:'#c23a28','stroke-width':1.6,'stroke-linecap':'round'});
- svgEl(g,'circle',{cx:0,cy:0,r:2.6,fill:'#8f2416'});return g;};
- const nT=needle(),nH=needle();
- nT.setAttribute('transform',`translate(${PT[0]},${PT[1]})`);
- nH.setAttribute('transform',`translate(${PH[0]},${PH[1]})`);
- let vT=72,vH=45;
- const verdict=()=>vT>78?'warm':vT<62?'cold':vH>60?'humid':vH<30?'dry':'right';
- const draw=()=>{
- nT.setAttribute('transform',`translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`);
- nH.setAttribute('transform',`translate(${PH[0]},${PH[1]}) rotate(${angH(vH).toFixed(1)})`);
- const v=verdict();
- for(const k of Object.keys(zones))zones[k].setAttribute('fill',k===v?'#c23a28':'#8f897b');
- setRd('R56',Math.round(vT)+'F · '+Math.round(vH)+'% RH · '+zones[v].textContent);};
- const half=(x,get,set,min,max)=>{const hit=svgEl(s,'rect',{x,y:8,width:65,height:106,fill:'transparent'});
- hit.style.cursor='ns-resize';
- dragDelta(hit,get,v=>{set(Math.max(min,Math.min(max,v)));draw();},{min,max,sens:(max-min)/110});};
- half(0,()=>vT,v=>vT=v,40,100);
- half(65,()=>vH,v=>vH=v,0,100);
- draw();})();
+/* R53-R56 extracted to widgets.js (GW.*) */
/* ---- screen-family chips: first round, the clearly-screen widgets ----
Defaults match each widget's shipped color (var fallbacks), so no family is applied until a chip is clicked.