diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-12 22:19:10 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-12 22:19:10 -0500 |
| commit | 145f86882d2a692e9049b0c7fd16f8a83303c326 (patch) | |
| tree | 7fce732fc5ce98cf54f9a49de6d4ffb207861ef4 /docs/prototypes/panel-widget-gallery.html | |
| parent | 63577703fe59f03e98a039fec8be56c2fc32826b (diff) | |
| download | archsetup-145f86882d2a692e9049b0c7fd16f8a83303c326.tar.gz archsetup-145f86882d2a692e9049b0c7fd16f8a83303c326.zip | |
refactor(gallery): extract meters R43, R44, R53-R56 into GW builders
The heading-bug servo chase and the chart recorder's day clock are widget-owned animations, so they move into their builders with the reduced-motion gates intact (reduced motion paints the recorder's full day once). This completes the Meters section — all 28 extracted.
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 247 |
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. |
