The page spent most of each second laying out the SVG, not simulating: the simulation itself costs ~26 ms per simulated hour. Profiled headless on Edge (CDP metrics and a timeline trace with invalidation tracking): - tokens asked the DOM for getPointAtLength right after writing a transform, which forced a layout of the whole SVG for every token, every frame (540 to 630 ms/s); - ~55 CSS stroke-dashoffset animations repainted the drawing all the time; - the press platens were SVG rects moved every frame, and any geometry write re-lays-out the whole SVG with its 275 text labels. Now the SVG holds only the machines and labels. Pipes, flow dashes and the conveyor are drawn on a canvas under it; tokens, records, platens, cakes, the extruder screw and plating bubbles on a canvas over it, both with the SVG's own viewBox transform, so pan and zoom need nothing extra. Routes are measured polylines (ptAt) instead of DOM path queries, and the 5 Hz tag, lamp and KPI writes only touch the DOM when the value changes. Headless Edge, 1600 x 900, before -> after: 1x 26 -> 60 fps, layout 842 -> 26 ms/s, long tasks 19 -> 0 60x 19 -> 59 fps, layout 858 -> 29 ms/s 600x 18 -> 60 fps, layout 855 -> 30 ms/s 3600x 33 -> 60 fps, layout 726 -> 29 ms/s Behaviour unchanged: all press states seen at 3600x, 47 stations open their detail, steam dip holds and recovers, lot and repress log, no text overflow at 1600 x 900 and 1258 x 800, canvas and SVG agree within 0.2 px at fit, zoomed, 1258 x 800 and phone size, both themes, no errors.
This commit is contained in:
1 parent
53f3a07a45
commit
f776025b58
1 file changed
+127
-56
+127
-56
@@ -38,10 +38,15 @@ main{flex:1 1 auto;min-height:0;display:flex;align-items:center;justify-content:
|
||||
svg{width:100%;height:100%;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}
|
||||
svg.zoomed{cursor:grab}
|
||||
svg.dragging{cursor:grabbing}
|
||||
.mapctl{position:absolute;right:12px;bottom:8px;display:flex;gap:2px;z-index:2}
|
||||
/* motion layers: pipes and flow under the drawing, tokens over it (see "effects" in the script) */
|
||||
.fx{position:absolute;left:0;top:0;pointer-events:none}
|
||||
#fxUnder{z-index:0}
|
||||
svg{position:relative;z-index:1}
|
||||
#fxOver{z-index:2}
|
||||
.mapctl{position:absolute;right:12px;bottom:8px;display:flex;gap:2px;z-index:3}
|
||||
.mapctl button{background:rgba(24,34,56,.88);min-width:30px}
|
||||
#btnInfo,footer .tabs{display:none}
|
||||
#hint{display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:var(--box);border:1px solid var(--stroke);border-radius:6px;padding:6px 10px;color:var(--mute);font-size:12px;white-space:nowrap;z-index:2}
|
||||
#hint{display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:var(--box);border:1px solid var(--stroke);border-radius:6px;padding:6px 10px;color:var(--mute);font-size:12px;white-space:nowrap;z-index:3}
|
||||
footer .tabs button.close{margin-left:auto;min-width:32px}
|
||||
footer{flex:0 0 clamp(150px,26vh,210px);display:grid;grid-template-columns:1.1fr 1fr 0.9fr;gap:10px;padding:8px 14px 10px;background:var(--panel);border-top:1px solid var(--stroke);min-height:0}
|
||||
.panel{background:var(--box);border:1px solid var(--stroke);border-radius:6px;padding:8px 12px;overflow:auto;min-height:0}
|
||||
@@ -239,12 +244,14 @@ html[data-theme=light] .screw{stroke:#7d8aa0}
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<canvas id="fxUnder" class="fx"></canvas>
|
||||
<svg id="svg" viewBox="0 0 2100 820" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="lPipes"></g>
|
||||
<g id="lMach"></g>
|
||||
<g id="lTok"></g>
|
||||
<g id="lTop"></g>
|
||||
</svg>
|
||||
<canvas id="fxOver" class="fx"></canvas>
|
||||
<div class="mapctl" id="zoom">
|
||||
<button id="zOut" title="Zoom out (-)">−</button>
|
||||
<button id="zIn" title="Zoom in (+). Pinch, wheel and drag also work">+</button>
|
||||
@@ -298,6 +305,15 @@ const pick=a=>a[Math.floor(Math.random()*a.length)];
|
||||
const f0=v=>Math.round(v).toString(),f1=v=>v.toFixed(1),f2=v=>v.toFixed(2);
|
||||
// first-order lag, stable for any step size
|
||||
const lag=(v,t,k,ds)=>v+(t-v)*(1-Math.exp(-k*ds));
|
||||
// Change-only writes: re-writing an unchanged text or attribute still makes the browser re-lay-out the SVG.
|
||||
const setT=(e,v)=>{v=String(v);if(e.__t!==v){e.__t=v;e.textContent=v;}};
|
||||
const setA=(e,k,v)=>{v=String(v);const c=e.__a||(e.__a={});if(c[k]!==v){c[k]=v;e.setAttribute(k,v);}};
|
||||
const setS=(e,k,v)=>{const c=e.__s||(e.__s={});if(c[k]!==v){c[k]=v;e.style[k]=v;}};
|
||||
// Everything that moves every frame (pipe flow, tokens, records, screw, bubbles) is drawn on two canvases,
|
||||
// one under and one over the SVG; the SVG keeps machines and labels. See "effects" further down.
|
||||
const FX={flows:[],screws:[],bubbles:[]},SPR=[];let fxDirty=true;
|
||||
function spr(kind,o){const t=Object.assign({kind,x:0,y:0,dead:false,cls:{}},o);
|
||||
t.remove=()=>{t.dead=true;};t.classList={add:c=>{t.cls[c]=true;}};SPR.push(t);return t;}
|
||||
const MIN=60,HOUR=3600,DAY=86400;
|
||||
function dur(s){s=Math.max(0,s);if(s<100)return f0(s)+' s';if(s<6000)return f0(s/60)+' min';if(s<2*DAY)return f1(s/HOUR)+' h';return f1(s/DAY)+' d';}
|
||||
const PALETTE=['#e0552f','#3aa0ff','#ffbf1f','#3ddc84','#c86bff','#ff7ab6','#59d9c9'];
|
||||
@@ -484,7 +500,7 @@ function pressEnter(P,st){P.state=st;P.tIn=0;
|
||||
if(st==='LOAD'){const T=P.set.T;if(!materialOk(T)){P.state='NOPVC';log(P.name+': no released '+(T.kind==='black'?'black':'colour')+' compound, holding','alarm');return;}
|
||||
P.cakeKg=T.wt/1000+rnd(0.02,0.04);consume(T,P.cakeKg);P.cake=true;P.peakT=0;P.loadD=rnd(4,6.5);S.labels.pairs--;
|
||||
if(S.labels.pairs<=0){S.labels.pairs=1800;S.labels.lot='L-'+(parseInt(S.labels.lot.slice(2))+1);log('Label oven: new pre-dried stack, lot '+S.labels.lot,'info');}
|
||||
if(vis()){const lb=el('circle',{r:3,fill:T.label,stroke:'#e6ecf7','stroke-width':1},L.tok);mover('l_'+P.id,lb,120,m=>m.node.remove());}}
|
||||
if(vis()){const lb=spr('dot',{r:3,fill:T.label,stroke:'#e6ecf7',sw:1});mover('l_'+P.id,lb,120,m=>m.node.remove());}}
|
||||
if(st==='STEAM')P.cake=false;
|
||||
}
|
||||
function pressTick(P,ds){
|
||||
@@ -650,11 +666,9 @@ function drawPress(m){const g=el('g',{class:'machine',id:'m_'+m.id});const {x,y,
|
||||
el('rect',{x:x+8,y:y+38,width:6,height:84,fill:'#2a3a56'},g);
|
||||
el('rect',{x:x+w-14,y:y+38,width:6,height:84,fill:'#2a3a56'},g);
|
||||
const px=x+20,pw=w-40;
|
||||
m.botPl=el('rect',{x:px,y:y+100,width:pw,height:16,class:'platen'},g);
|
||||
el('rect',{x:px+8,y:y+97,width:pw-16,height:3,fill:'#b9c2cc'},g);
|
||||
m.topPl=el('rect',{x:px,y:y+38,width:pw,height:16,class:'platen'},g);
|
||||
m.topSt=el('rect',{x:px+8,y:y+54,width:pw-16,height:3,fill:'#b9c2cc'},g);
|
||||
m.cake=el('g',{visibility:'hidden'},g);el('circle',{cx:x+w/2,cy:y+90,r:7,fill:'#3d3a33',stroke:'#8a7a55'},m.cake);
|
||||
// platens, top stamper and cake move every cycle: drawn on the overlay canvas (drawFx), not in the SVG
|
||||
m.plat={px,pw,yb:y+100,cx:x+w/2,cy:y+90};
|
||||
m.pY=[y+38,y+80];
|
||||
m.stateEl=el('text',{x:x+w/2,y:y+138,class:'mtag','text-anchor':'middle',style:'font-weight:700;fill:var(--ink)'},g,'OFF');
|
||||
m.tagEls=[0,1,2].map(i=>el('text',{x:x+7,y:y+154+i*13,class:'mtag'},g,''));
|
||||
@@ -671,7 +685,7 @@ function drawVex(m){const g=el('g',{class:'machine',id:'m_'+m.id});const {x,y,w,
|
||||
el('text',{x:x+w-8,y:y+82,class:'small','text-anchor':'end'},g,'cake B ▶');
|
||||
m.zoneEls=[0,1,2].map(i=>el('rect',{x:x+10+i*40,y:y+88,width:38,height:26,class:'zone',style:'fill:var(--z0)'},g));
|
||||
m.zoneT=[0,1,2].map(i=>el('text',{x:x+29+i*40,y:y+105,class:'ctxt','text-anchor':'middle'},g,''));
|
||||
el('line',{x1:x+10,y1:y+120,x2:x+w-10,y2:y+120,class:'screw'},g);
|
||||
FX.screws.push({x1:x+10,x2:x+w-10,y:y+120});
|
||||
m.g=g;g.addEventListener('click',()=>select(m.id));}
|
||||
function drawPppu(m){m.tagY=m.y+126;const g=drawBox(m);
|
||||
m.pos=[0,1,2,3].map(i=>{const px=m.x+7+(i&1)*68,py=m.y+36+(i>>1)*40;const r=el('rect',{x:px,y:py,width:64,height:36,rx:2,class:'pos'},g);
|
||||
@@ -680,7 +694,7 @@ function drawPdpu(m){m.tagY=m.y+126;const g=drawBox(m);
|
||||
m.cells=[0,1].map(i=>{const cx=m.x+7+i*74,cy=m.y+38,cw=70,ch=76;const cg=el('g',{class:'cell'},g);
|
||||
el('rect',{x:cx,y:cy,width:cw,height:ch,rx:2,class:'cellr'},cg);
|
||||
el('rect',{x:cx+3,y:cy+20,width:cw-6,height:ch-23,class:'liquid'},cg);
|
||||
for(let b=0;b<3;b++)el('circle',{cx:cx+10+b*18,cy:cy+ch-5,r:1.6,class:'bub',style:'animation-delay:'+rnd(0,2.2).toFixed(2)+'s'},cg);
|
||||
for(let b=0;b<3;b++)FX.bubbles.push({x:cx+10+b*18,y:cy+ch-5,d:rnd(0,2.2),cell:(m.id==='pdpu2'?2:0)+i});
|
||||
el('rect',{x:cx+cw-11,y:cy+22,width:6,height:50,class:'thk'},cg);
|
||||
const tv=el('rect',{x:cx+cw-11,y:cy+72,width:6,height:0,class:'thkv'},cg);
|
||||
const t=[0,1,2,3].map(k=>el('text',{x:cx+4,y:cy+12+k*15,class:'ctxt'},cg,''));
|
||||
@@ -697,9 +711,21 @@ const GATES={g1:gate('g1',160,116,'gate 1'),g2:gate('g2',330,116,'gate 2'),g3:ga
|
||||
function flash(id){if(S.quiet)return;GATES[id].flash=2;GATES[id].g.classList.add('ok');}
|
||||
|
||||
/* pipes and routes */
|
||||
// Routes are polylines (M/H/V/L only) measured here, so a token never asks the DOM for a point on a
|
||||
// path: getPointAtLength after a transform write forces a layout of the whole SVG on every call.
|
||||
const PATHS={};
|
||||
function route(id,d,cls){PATHS[id]=el('path',{d,class:'flow '+cls},L.pipes);PATHS[id].len=PATHS[id].getTotalLength();}
|
||||
function pipe(d,cls,thin){el('path',{d,class:'pipe '+cls},L.pipes);el('path',{d,class:'flow '+cls+(thin?' thin':'')},L.pipes);}
|
||||
function poly(d){if(/[^MHVL\d\s,.\-]/.test(d))throw new Error('route needs M/H/V/L only: '+d);
|
||||
const segs=[],re=/([MHVL])\s*(-?[\d.]+)(?:\s*,\s*(-?[\d.]+))?/g;let x=0,y=0,len=0,sx=0,sy=0,m;
|
||||
while((m=re.exec(d))){const c=m[1],a=+m[2],b=m[3]==null?0:+m[3];
|
||||
if(c==='M'){x=sx=a;y=sy=b;continue;}const nx=c==='V'?x:a,ny=c==='H'?y:c==='V'?a:b,l=Math.hypot(nx-x,ny-y);
|
||||
segs.push({x0:x,y0:y,x1:nx,y1:ny,l0:len,l});len+=l;x=nx;y=ny;}
|
||||
return {segs,len,sx,sy};}
|
||||
function ptAt(p,pos){const g0=p.segs;if(!g0.length)return {x:p.sx,y:p.sy};
|
||||
for(const g of g0)if(pos<=g.l0+g.l){const k=g.l?Math.max(0,pos-g.l0)/g.l:0;return {x:g.x0+(g.x1-g.x0)*k,y:g.y0+(g.y1-g.y0)*k};}
|
||||
const g=g0[g0.length-1];return {x:g.x1,y:g.y1};}
|
||||
function flow(d,cls){const c=cls.split(' ');FX.flows.push({k:'flow',c:c[0],thin:c.includes('thin'),p2:new Path2D(d)});}
|
||||
function route(id,d,cls){PATHS[id]=poly(d);flow(d,cls);}
|
||||
function pipe(d,cls,thin){FX.flows.push({k:'pipe',c:cls,p2:new Path2D(d)});flow(d,cls+(thin?' thin':''));}
|
||||
// pipe rack y 616 to 650, drops into each press from below
|
||||
pipe('M100,676 V616 H1074','steam');['456','740','912','1074'].forEach(x=>pipe(`M${x},616 V512`,'steam'));
|
||||
pipe('M1080,621 H112 V676','cond',true);
|
||||
@@ -710,8 +736,8 @@ pipe('M574,676 V636 H444 V512','hyd');pipe('M574,636 H728 V512','hyd');
|
||||
pipe('M300,676 V644 H2088 V34 H845 V46','chw',true);pipe('M1013,34 V46','chw',true);
|
||||
pipe('M716,676 V650 H2094 V40 H544 V46','di',true);pipe('M1162,40 V46','di',true);
|
||||
// record conveyor and edge-trim collection
|
||||
el('path',{d:'M370,590 H1772',class:'conv'},L.pipes);el('path',{d:'M370,590 H1772',class:'convd'},L.pipes);
|
||||
el('path',{d:'M392,574 H1104',class:'flow scrap thin'},L.pipes);
|
||||
FX.flows.push({k:'conv',p2:new Path2D('M370,590 H1772')},{k:'convd',p2:new Path2D('M370,590 H1772')});
|
||||
flow('M392,574 H1104','scrap thin');
|
||||
// material
|
||||
route('s_v','M47,436 V444 H272 V428','mat thin');route('s_r','M107,436 V444 H272 V428','reg thin');route('s_c','M167,436 V444 H272 V428','col thin');
|
||||
route('f_vex','M272,316 V308 H554 V366','mat thin');route('f_c1','M272,316 V308 H859 V316','mat thin');route('f_c2','M272,316 V308 H1021 V316','mat thin');
|
||||
@@ -728,15 +754,10 @@ route('carton','M1685,408 V430 H1483 V452','route thin');
|
||||
|
||||
/* ================================================================ tokens */
|
||||
const movers=[];
|
||||
function mover(pid,node,v,onEnd){const p=PATHS[pid];const m={p,node,pos:0,v,onEnd};movers.push(m);place(node,p.getPointAtLength(0));return m;}
|
||||
function place(node,pt){node.setAttribute('transform',`translate(${pt.x.toFixed(1)},${pt.y.toFixed(1)})`);}
|
||||
function dot(fill,r){return el('circle',{r:r||2.4,fill},L.tok);}
|
||||
function shape(kind){const g=el('g',{},L.tok);
|
||||
if(kind==='L'){el('circle',{r:7,fill:'#1a1408',stroke:'#c9a24a'},g);el('circle',{r:2.4,fill:'#c9a24a'},g);}
|
||||
else if(kind==='Ls'){el('circle',{r:7,fill:'#d9dee6',stroke:'#fff'},g);el('circle',{r:2.4,fill:'#8a97b0'},g);}
|
||||
else if(kind==='S'){el('circle',{r:7,fill:'#b9c2cc',stroke:'#e6ecf7'},g);el('circle',{r:4.5,fill:'none',stroke:'#7d8794'},g);el('circle',{r:1.6,fill:'#3a4658'},g);}
|
||||
else{el('circle',{r:7,fill:'#8e98a6',stroke:'#c8d2e4'},g);el('circle',{r:2.4,fill:'#c8d2e4'},g);}
|
||||
return g;}
|
||||
function mover(pid,node,v,onEnd){const p=PATHS[pid];const m={p,node,pos:0,v,onEnd};movers.push(m);place(node,ptAt(p,0));return m;}
|
||||
function place(node,pt){node.x=pt.x;node.y=pt.y;}
|
||||
function dot(fill,r){return spr('dot',{r:r||2.4,fill});}
|
||||
function shape(kind){return spr('shape',{k:kind});}
|
||||
const hops=[];
|
||||
function ctr(id){const m=M[id];return m?{x:m.x+m.w/2,y:m.y+m.h/2}:null;}
|
||||
function hop(from,to,it){if(!visHop()||!from)return;const a=ctr(from),b=ctr(to);if(!a||!b)return;
|
||||
@@ -744,7 +765,7 @@ function hop(from,to,it){if(!visHop()||!from)return;const a=ctr(from),b=ctr(to);
|
||||
function routeHop(pid,kind){mover(pid,shape(kind),320,m=>m.node.remove());}
|
||||
const RECS=[];const CONV0=370,CONVEND=1772,CONVY=590;const DROPX={pDA:392,pDB:676,pC1:839,pC2:1001};
|
||||
const STATIONS=[{id:'optical',at:1185,dwell:2.2},{id:'finish',at:1329,dwell:2.5},{id:'pack',at:1483,dwell:3},{id:'ship',at:1672,dwell:0.5}];
|
||||
function mkRec(P,tp){const T=P.set.T;const g=el('g',{class:'rec'+(tp?' tp':'')},L.tok);el('circle',{r:9,class:'disc',style:'fill:'+T.vinyl},g);el('circle',{r:3.2,fill:T.label},g);
|
||||
function mkRec(P,tp){const T=P.set.T;const g=spr('rec',{r:9,lr:3.2,vinyl:T.vinyl,label:T.label});if(tp)g.classList.add('tp');
|
||||
const r={node:g,T,tp,ph:'trim',dwell:6,x:DROPX[P.id],y:544,pos:0,visited:{},n:P.cyc,pname:P.name};RECS.push(r);place(g,r);return r;}
|
||||
function recTick(ds){
|
||||
const onC=RECS.filter(r=>r.ph==='conv'&&!r.gone).sort((a,b)=>b.pos-a.pos);
|
||||
@@ -764,7 +785,7 @@ function recStation(st,r){const T=r.T;
|
||||
log('Optical: '+r.pname+' record #'+r.n+' rejected ('+d+'), label punched, to the granulator','alarm');
|
||||
mover('reject',r.node,90,m=>{S.punched++;S.punchUntil=S.t+8;m.node.remove();mover('p2g',dot('#c2b280',2.6),40,mm=>{mm.node.remove();S.gran.bin+=T.wt/1000;});});return;}
|
||||
S.opticalLast='pass #'+r.n;
|
||||
if(r.tp||Math.random()<0.02){const g=el('g',{class:'rec'+(r.tp?' tp':'')},L.tok);el('circle',{r:7,class:'disc',style:'fill:'+T.vinyl},g);el('circle',{r:2.4,fill:T.label},g);
|
||||
if(r.tp||Math.random()<0.02){const g=spr('rec',{r:7,lr:2.4,vinyl:T.vinyl,label:T.label});if(r.tp)g.classList.add('tp');
|
||||
const tp=r.tp;mover('sample',g,60,m=>{m.node.remove();S.samples++;const sc=jit(98.2,1.2);S.audioLast=f1(sc);if(tp)S.tpToArtist++;
|
||||
log('Audio compare: '+T.sid+(tp?' test pressing':' record #'+r.n)+' score '+f1(sc)+', clicks '+f0(rnd(0,3))+'/side, noise floor OK'+(tp?', to the artist':''),tp?'warn':'ok');});
|
||||
if(r.tp){r.gone=true;r.node.remove();}}
|
||||
@@ -788,7 +809,7 @@ function matTick(){
|
||||
if(G.run&&S.t>=G.end){S.silo.r=Math.min(S.silo.rcap,S.silo.r+G.run*0.99);G.total+=G.run;log('Granulator 400: '+f1(G.run)+' kg ground to regrind, to the silo','info');
|
||||
if(visHop())for(let i=0;i<3;i++)setTimeout(()=>mover('regrind',dot('#8ea36f',2.6),220,m=>m.node.remove()),i*250);G.run=0;}
|
||||
if(!G.run&&G.bin>=12&&shiftOn()){G.run=G.bin;G.bin=0;G.end=S.t+G.run/225*HOUR;
|
||||
if(visHop())mover('trimcart',el('rect',{x:-7,y:-5,width:14,height:10,fill:'#5a4f33',stroke:'#c2b280'},L.tok),160,m=>m.node.remove());}
|
||||
if(visHop())mover('trimcart',spr('rect',{w:14,h:10,fill:'#5a4f33',stroke:'#c2b280'}),160,m=>m.node.remove());}
|
||||
}
|
||||
|
||||
/* ================================================================ utilities */
|
||||
@@ -830,46 +851,44 @@ function tick(ds){S.t+=ds;
|
||||
/* ================================================================ render */
|
||||
let feedAcc=0,cartonAcc=0;
|
||||
function animate(dt){
|
||||
for(let i=movers.length-1;i>=0;i--){const m=movers[i];m.pos+=m.v*dt;if(m.pos>=m.p.len){place(m.node,m.p.getPointAtLength(m.p.len));movers.splice(i,1);m.onEnd&&m.onEnd(m);}else place(m.node,m.p.getPointAtLength(m.pos));}
|
||||
for(let i=movers.length-1;i>=0;i--){const m=movers[i];m.pos+=m.v*dt;if(m.pos>=m.p.len){place(m.node,ptAt(m.p,m.p.len));movers.splice(i,1);m.onEnd&&m.onEnd(m);}else place(m.node,ptAt(m.p,m.pos));}
|
||||
for(let i=hops.length-1;i>=0;i--){const h=hops[i];h.t+=dt;const k=Math.min(1,h.t/h.d);place(h.n,{x:h.a.x+(h.b.x-h.a.x)*k,y:h.a.y+(h.b.y-h.a.y)*k-Math.sin(k*Math.PI)*18});if(k>=1){h.n.remove();hops.splice(i,1);}}
|
||||
RECS.forEach(r=>{if(!r.gone)place(r.node,r.ph==='conv'?{x:CONV0+r.pos,y:CONVY}:{x:r.x,y:r.y});});
|
||||
if(S.running&&!S.quiet&&S.speed<=600){feedAcc+=dt;
|
||||
if(feedAcc>0.7&&PRESS.some(running)){feedAcc=0;const k=pick(['s_v','s_v','s_r','s_c','f_vex','f_c1','f_c2']);
|
||||
const col=k==='s_r'?'#8ea36f':k==='s_c'?'#c86bff':'#b9c2d0';mover(k,dot(col),110,m=>m.node.remove());
|
||||
if(S.ex35&&Math.random()<0.5)mover('f_ex35',dot('#c86bff'),110,m=>{m.node.remove();mover('ex2vex',dot('#c86bff'),20,mm=>mm.node.remove());});}
|
||||
cartonAcc+=dt;if(cartonAcc>7){cartonAcc=0;mover('carton',el('rect',{x:-6,y:-4,width:12,height:8,fill:'#8a6d3b',stroke:'#c9a24a'},L.tok),50,m=>m.node.remove());}}
|
||||
cartonAcc+=dt;if(cartonAcc>7){cartonAcc=0;mover('carton',spr('rect',{w:12,h:8,fill:'#8a6d3b',stroke:'#c9a24a'}),50,m=>m.node.remove());}}
|
||||
for(const k in GATES){const G=GATES[k];if(G.flash>0){G.flash-=dt;if(G.flash<=0)G.g.classList.remove('ok');}}
|
||||
PRESS.forEach(P=>{const m=M[P.id];const ty=m.pY[0]+(m.pY[1]-m.pY[0])*(1-P.open);m.topPl.setAttribute('y',ty.toFixed(1));m.topSt.setAttribute('y',(ty+16).toFixed(1));
|
||||
const heat=P.state==='STEAM',cool=P.state==='COOL';m.topPl.classList.toggle('heat',heat);m.botPl.classList.toggle('heat',heat);m.topPl.classList.toggle('cool',cool);m.botPl.classList.toggle('cool',cool);
|
||||
m.cake.setAttribute('visibility',P.cake?'visible':'hidden');});
|
||||
PRESS.forEach(P=>{const k=1-Math.exp(-6*dt);P.__h=(P.__h||0)+((P.state==='STEAM'?1:0)-(P.__h||0))*k;P.__c=(P.__c||0)+((P.state==='COOL'?1:0)-(P.__c||0))*k;});
|
||||
}
|
||||
const lampSet=(id,cls)=>{const m=M[id];if(m&&m.lamp)m.lamp.setAttribute('class','lamp '+cls);};
|
||||
const lampSet=(id,cls)=>{const m=M[id];if(m&&m.lamp)setA(m.lamp,'class','lamp '+cls);};
|
||||
function pressLabel(P){if(P.state==='STOP')return 'STOP';if(P.state==='STEAM'&&P.waiting)return 'STEAM · WAIT';if(P.state==='CHANGE')return P.restart?'WARM-UP':'STAMPER CHANGE';if(P.state==='NOPVC')return 'NO PVC';return P.state;}
|
||||
function renderTags(){
|
||||
MACH.forEach(m=>{if(m.tags&&m.tagEls)m.tags.forEach((fn,i)=>{if(m.tagEls[i])m.tagEls[i].textContent=fn();});if(m.barEl)m.barEl.setAttribute('width',((m.w-14)*stFrac(m.st)).toFixed(1));});
|
||||
MACH.forEach(m=>{if(m.tags&&m.tagEls)m.tags.forEach((fn,i)=>{if(m.tagEls[i])setT(m.tagEls[i],fn());});if(m.barEl)setA(m.barEl,'width',((m.w-14)*stFrac(m.st)).toFixed(1));});
|
||||
[['silo_v',S.silo.v/S.silo.vcap],['silo_r',S.silo.r/S.silo.rcap],['silo_c',S.silo.c/S.silo.ccap]].forEach(([id,l])=>{const m=M[id];l=Math.max(0,Math.min(1,l));const hh=(m.h-32)*l;
|
||||
m.fillEl.setAttribute('y',(m.y+m.h-30-hh).toFixed(1));m.fillEl.setAttribute('height',hh.toFixed(1));m.lvlEl.textContent=f0(l*100)+' %';m.lamp.setAttribute('class','lamp '+(l<0.1?'alarm':l<0.25?'warn':'run'));});
|
||||
setA(m.fillEl,'y',(m.y+m.h-30-hh).toFixed(1));setA(m.fillEl,'height',hh.toFixed(1));setT(m.lvlEl,f0(l*100)+' %');setA(m.lamp,'class','lamp '+(l<0.1?'alarm':l<0.25?'warn':'run'));});
|
||||
PRESS.forEach(P=>{const m=M[P.id];const T=P.set&&P.set.T;
|
||||
m.stateEl.textContent=pressLabel(P);const amber=P.waiting||['CHANGE','UNMOUNT'].includes(P.state)||P.mode==='tp';
|
||||
m.stateEl.style.fill=P.state==='NOPVC'||P.state==='STOP'?'var(--alarm)':amber?'var(--warn)':(P.state==='IDLE'||P.state==='OFF')?'var(--mute)':'var(--ink)';
|
||||
m.tagEls[0].textContent=running(P)?'F '+f0(P.force)+' t · '+f0(P.moldT)+' °C':P.state==='STOP'?P.stopWhy:P.state==='OFF'?'night: shift 06:00':'mould '+f0(P.moldT)+' °C';
|
||||
m.tagEls[1].textContent=T?T.id+' · '+P.set.k:(P.state==='OFF'?'':'waiting for a set');
|
||||
m.tagEls[2].textContent=!T?'':P.mode==='tp'?'test press '+(TP_N-P.tpLeft)+'/'+TP_N:P.mode==='fa'?'first article':'run '+T.cycles+'/'+T.target;
|
||||
setT(m.stateEl,pressLabel(P));const amber=P.waiting||['CHANGE','UNMOUNT'].includes(P.state)||P.mode==='tp';
|
||||
setS(m.stateEl,'fill',P.state==='NOPVC'||P.state==='STOP'?'var(--alarm)':amber?'var(--warn)':(P.state==='IDLE'||P.state==='OFF')?'var(--mute)':'var(--ink)');
|
||||
setT(m.tagEls[0],running(P)?'F '+f0(P.force)+' t · '+f0(P.moldT)+' °C':P.state==='STOP'?P.stopWhy:P.state==='OFF'?'night: shift 06:00':'mould '+f0(P.moldT)+' °C');
|
||||
setT(m.tagEls[1],T?T.id+' · '+P.set.k:(P.state==='OFF'?'':'waiting for a set'));
|
||||
setT(m.tagEls[2],!T?'':P.mode==='tp'?'test press '+(TP_N-P.tpLeft)+'/'+TP_N:P.mode==='fa'?'first article':'run '+T.cycles+'/'+T.target);
|
||||
lampSet(P.id,P.state==='NOPVC'||P.state==='STOP'?'alarm':amber?'warn':running(P)?'run':'idle');});
|
||||
['tA','tB','tC1','tC2'].forEach((id,i)=>lampSet(id,running(PRESS[i])?'run':'idle'));
|
||||
const dRun=PRESS.slice(0,2).some(running);
|
||||
M.vex.zoneT.forEach((t,i)=>t.textContent=f0(dRun?jit([132,146,156][i],1):[92,96,98][i])+'°');
|
||||
M.vex.zoneEls.forEach((z,i)=>{z.style.fill=dRun?'var(--z'+(i+1)+')':'var(--z0)';});lampSet('vex',dRun?'run':'idle');lampSet('ex35',S.ex35?'run':'idle');
|
||||
M.pppu.pos.forEach((p,i)=>{const w=ST.pppu.slots[i];p.r.setAttribute('class','pos'+(w?' on':''));p.a.textContent=w?w.it.T.sid+w.it.side+' 20 A':'free';p.b.textContent=w?dur(w.end-S.t):'';});
|
||||
M.vex.zoneT.forEach((t,i)=>setT(t,f0(dRun?jit([132,146,156][i],1):[92,96,98][i])+'°'));
|
||||
M.vex.zoneEls.forEach((z,i)=>{setS(z,'fill',dRun?'var(--z'+(i+1)+')':'var(--z0)');});lampSet('vex',dRun?'run':'idle');lampSet('ex35',S.ex35?'run':'idle');
|
||||
M.pppu.pos.forEach((p,i)=>{const w=ST.pppu.slots[i];setA(p.r,'class','pos'+(w?' on':''));setT(p.a,w?w.it.T.sid+w.it.side+' 20 A':'free');setT(p.b,w?dur(w.end-S.t):'');});
|
||||
lampSet('pppu',ST.pppu.slots.some(Boolean)?'run':ST.pppu.q.length?'warn':'idle');
|
||||
[0,1].forEach(u=>{const m=M['pdpu'+(u+1)];let any=false,done=false;
|
||||
m.cells.forEach((cv,i)=>{const c=CELLS[u*2+i];const j=c.job;cv.g.setAttribute('class','cell'+(c.ph==='plate'?' on':c.ph==='done'?' done':''));
|
||||
m.cells.forEach((cv,i)=>{const c=CELLS[u*2+i];const j=c.job;setA(cv.g,'class','cell'+(c.ph==='plate'?' on':c.ph==='done'?' done':''));
|
||||
if(c.ph!=='idle')any=true;if(c.ph==='done')done=true;
|
||||
const hh=50*(j?Math.min(1,c.mass/c.target):0);cv.tv.setAttribute('y',(cv.cy+72-hh).toFixed(1));cv.tv.setAttribute('height',hh.toFixed(1));
|
||||
cv.t[0].textContent=j?jl(j).replace(' ',''):'idle';
|
||||
cv.t[1].textContent=c.ph==='plate'?f0(c.I)+' A':c.ph==='load'?'load':c.ph==='unload'?'unload':c.ph==='done'?'DONE':'';
|
||||
cv.t[2].textContent=j?f2(c.mass/massFor(1))+' mm':'';
|
||||
cv.t[3].textContent=j&&c.ph!=='done'?dur(cellLeft(c)):c.ph==='done'?'waits':'';});
|
||||
const hh=50*(j?Math.min(1,c.mass/c.target):0);setA(cv.tv,'y',(cv.cy+72-hh).toFixed(1));setA(cv.tv,'height',hh.toFixed(1));
|
||||
setT(cv.t[0],j?jl(j).replace(' ',''):'idle');
|
||||
setT(cv.t[1],c.ph==='plate'?f0(c.I)+' A':c.ph==='load'?'load':c.ph==='unload'?'unload':c.ph==='done'?'DONE':'');
|
||||
setT(cv.t[2],j?f2(c.mass/massFor(1))+' mm':'');
|
||||
setT(cv.t[3],j&&c.ph!=='done'?dur(cellLeft(c)):c.ph==='done'?'waits':'');});
|
||||
lampSet('pdpu'+(u+1),done?'warn':any?'run':'idle');});
|
||||
['pasu','pmpb','qc','vsf','vsp','vsc'].forEach(id=>lampSet(id,ST[id].slots[0]?'run':ST[id].q.length?'warn':'idle'));
|
||||
[['audio','intake'],['master','master'],['cut','cut']].forEach(([id,st])=>lampSet(id,cntStage(st)?'run':'idle'));
|
||||
@@ -879,13 +898,13 @@ function renderTags(){
|
||||
lampSet('lab',S.lab.test||S.lab.queue.length?'warn':'run');lampSet('oven',anyRun?'run':'idle');lampSet('carton','run');
|
||||
lampSet('boiler',S.boiler.p<9.5?'alarm':S.boiler.p<9.65?'warn':'run');lampSet('chiller',S.chiller.sup>24?'warn':'run');
|
||||
['comp','diro','zld','energy','trace','hvac','exh'].forEach(id=>lampSet(id,'run'));lampSet('hyd',dRun?'run':'idle');
|
||||
GATES.g1.c.textContent=cntStage('gate1')+' waiting';GATES.g2.c.textContent=cntStage('gate2')+' waiting';GATES.g3.c.textContent=TITLES.filter(T=>T.tp==='sent').length+' waiting';
|
||||
$('kClock').textContent=clockStr();$('kShift').textContent=shiftOn()?(hourOf()<14?'1 (06-14)':'2 (14-22)'):'night';
|
||||
$('kPress').textContent=PRESS.filter(running).length+'/4';
|
||||
$('kRate').textContent=S.cyc.length;$('kGood').textContent=S.day.good;$('kRej').textContent=S.day.rej;$('kYield').textContent=yieldStr();
|
||||
$('kGalv').textContent=galvCount();$('kRack').textContent=rackReady();
|
||||
$('kSteam').textContent=f2(S.boiler.p)+' bar';$('kSteam').style.color=S.boiler.p<9.5?'#ff3b3b':'';
|
||||
$('kLoad').textContent=f0(S.energy.load)+' kW';$('kPV').textContent=f0(S.energy.pv)+' kW';
|
||||
setT(GATES.g1.c,cntStage('gate1')+' waiting');setT(GATES.g2.c,cntStage('gate2')+' waiting');setT(GATES.g3.c,TITLES.filter(T=>T.tp==='sent').length+' waiting');
|
||||
setT($('kClock'),clockStr());setT($('kShift'),shiftOn()?(hourOf()<14?'1 (06-14)':'2 (14-22)'):'night');
|
||||
setT($('kPress'),PRESS.filter(running).length+'/4');
|
||||
setT($('kRate'),S.cyc.length);setT($('kGood'),S.day.good);setT($('kRej'),S.day.rej);setT($('kYield'),yieldStr());
|
||||
setT($('kGalv'),galvCount());setT($('kRack'),rackReady());
|
||||
setT($('kSteam'),f2(S.boiler.p)+' bar');setS($('kSteam'),'color',S.boiler.p<9.5?'#ff3b3b':'');
|
||||
setT($('kLoad'),f0(S.energy.load)+' kW');setT($('kPV'),f0(S.energy.pv)+' kW');
|
||||
}
|
||||
|
||||
/* ================================================================ log */
|
||||
@@ -960,7 +979,7 @@ document.addEventListener('keydown',e=>{if(e.code==='Space'&&e.target===document
|
||||
const SV=$('svg'),BASE={w:2100,h:820},ZMAX=5,VB={x:0,y:0,w:BASE.w};
|
||||
function applyVB(){VB.w=Math.min(BASE.w,Math.max(BASE.w/ZMAX,VB.w));const h=VB.w*BASE.h/BASE.w;
|
||||
VB.x=Math.min(BASE.w-VB.w,Math.max(0,VB.x));VB.y=Math.min(BASE.h-h,Math.max(0,VB.y));
|
||||
SV.setAttribute('viewBox',VB.x.toFixed(1)+' '+VB.y.toFixed(1)+' '+VB.w.toFixed(1)+' '+h.toFixed(1));SV.classList.toggle('zoomed',VB.w<BASE.w-0.5);}
|
||||
SV.setAttribute('viewBox',VB.x.toFixed(1)+' '+VB.y.toFixed(1)+' '+VB.w.toFixed(1)+' '+h.toFixed(1));SV.classList.toggle('zoomed',VB.w<BASE.w-0.5);fxDirty=true;}
|
||||
function svgPt(cx,cy){const m=SV.getScreenCTM().inverse();return {x:m.a*cx+m.c*cy+m.e,y:m.b*cx+m.d*cy+m.f};}
|
||||
function zoomAt(cx,cy,f){const p=svgPt(cx,cy),nw=Math.min(BASE.w,Math.max(BASE.w/ZMAX,VB.w/f)),r=nw/VB.w;VB.x=p.x-(p.x-VB.x)*r;VB.y=p.y-(p.y-VB.y)*r;VB.w=nw;applyVB();}
|
||||
function zoomMid(f){const b=SV.getBoundingClientRect();zoomAt(b.left+b.width/2,b.top+b.height/2,f);}
|
||||
@@ -990,6 +1009,57 @@ $('btnInfo').addEventListener('click',()=>showPanel(document.body.dataset.panel|
|
||||
const HDR=document.querySelector('header');
|
||||
new ResizeObserver(()=>document.documentElement.style.setProperty('--hdrH',HDR.offsetHeight+'px')).observe(HDR);
|
||||
|
||||
/* ================================================================ effects
|
||||
Two canvases sized to the SVG box and drawn in viewBox units with the same xMidYMid-meet transform as
|
||||
the SVG, so pan and zoom need nothing extra. Under the SVG: pipes, flow dashes and the conveyor (under
|
||||
the machines, as before). Over it: tokens, records, extruder screw, plating bubbles. Nothing here
|
||||
touches the DOM per frame, which is what made the page lag (forced layouts of the whole SVG). */
|
||||
const CU=$('fxUnder'),CO=$('fxOver'),XU=CU.getContext('2d'),XO=CO.getContext('2d'),BOX={w:1,h:1,d:1};
|
||||
let fxT=0,screwPh=0,COL={};
|
||||
function fxColors(){const de=document.documentElement,cs=getComputedStyle(de),lt=de.getAttribute('data-theme')==='light';
|
||||
COL={plat:lt?{b:[199,209,223],h:[224,112,79],c:[111,163,216],s:'#8e9db4'}:{b:[42,58,86],h:[179,64,44],c:[44,91,143],s:'#4a5f85'},pipeA:lt?0.32:0.28,conv:lt?'#dfe5ee':'#1c2740',convd:lt?'#b5c1d4':'#2e4066',screw:lt?'#7d8aa0':'#6d7d99',bub:lt?'#3f97cc':'#8fd3ff'};
|
||||
['steam','cond','cw','cwr','hyd','chw','di','mat','reg','col','metal','scrap','route','lbl'].forEach(k=>{COL[k]=cs.getPropertyValue('--p-'+k).trim();});fxDirty=true;}
|
||||
function fxResize(){const mr=SV.parentNode.getBoundingClientRect(),sr=SV.getBoundingClientRect();
|
||||
BOX.w=Math.max(1,sr.width);BOX.h=Math.max(1,sr.height);BOX.d=Math.min(2,window.devicePixelRatio||1);
|
||||
for(const c of [CU,CO]){c.style.left=(sr.left-mr.left)+'px';c.style.top=(sr.top-mr.top)+'px';c.style.width=BOX.w+'px';c.style.height=BOX.h+'px';
|
||||
c.width=Math.round(BOX.w*BOX.d);c.height=Math.round(BOX.h*BOX.d);}fxDirty=true;}
|
||||
function fxBegin(x){const vh=VB.w*BASE.h/BASE.w,k=Math.min(BOX.w/VB.w,BOX.h/vh),d=BOX.d;
|
||||
x.setTransform(1,0,0,1,0,0);x.clearRect(0,0,x.canvas.width,x.canvas.height);
|
||||
x.setTransform(d*k,0,0,d*k,d*((BOX.w-VB.w*k)/2-VB.x*k),d*((BOX.h-vh*k)/2-VB.y*k));}
|
||||
function circ(x,cx,cy,r,fill,stroke,sw){x.beginPath();x.arc(cx,cy,r,0,6.2832);if(fill){x.fillStyle=fill;x.fill();}if(stroke){x.lineWidth=sw||1;x.strokeStyle=stroke;x.stroke();}}
|
||||
const SHAPES={L:[[7,'#1a1408','#c9a24a'],[2.4,'#c9a24a']],Ls:[[7,'#d9dee6','#fff'],[2.4,'#8a97b0']],
|
||||
S:[[7,'#b9c2cc','#e6ecf7'],[4.5,null,'#7d8794'],[1.6,'#3a4658']],M:[[7,'#8e98a6','#c8d2e4'],[2.4,'#c8d2e4']]};
|
||||
function drawFx(dt){fxT+=dt;screwPh+=dt*(S.vex.rpm>1?17.8:0);
|
||||
const u=XU;fxBegin(u);u.lineJoin='round';
|
||||
for(const f of FX.flows){
|
||||
if(f.k==='pipe'){u.globalAlpha=COL.pipeA;u.strokeStyle=COL[f.c];u.lineWidth=5;u.lineCap='round';u.setLineDash([]);}
|
||||
else if(f.k==='conv'){u.globalAlpha=1;u.strokeStyle=COL.conv;u.lineWidth=14;u.lineCap='butt';u.setLineDash([]);}
|
||||
else if(f.k==='convd'){u.globalAlpha=1;u.strokeStyle=COL.convd;u.lineWidth=14;u.lineCap='butt';u.setLineDash([3,9]);u.lineDashOffset=-(fxT*10%12);}
|
||||
else{u.globalAlpha=1;u.strokeStyle=COL[f.c];u.lineCap='round';
|
||||
if(f.thin){u.lineWidth=1.4;u.setLineDash([3,7]);u.lineDashOffset=-(fxT*13.3%10);}else{u.lineWidth=2.2;u.setLineDash([7,9]);u.lineDashOffset=-(fxT*13.3%16);}}
|
||||
u.stroke(f.p2);}
|
||||
u.globalAlpha=1;
|
||||
const o=XO;fxBegin(o);o.setLineDash([6,6]);o.lineDashOffset=-(screwPh%12);o.lineWidth=2;o.strokeStyle=COL.screw;o.lineCap='butt';
|
||||
for(const w of FX.screws){o.beginPath();o.moveTo(w.x1,w.y);o.lineTo(w.x2,w.y);o.stroke();}
|
||||
o.setLineDash([]);o.globalAlpha=0.7;
|
||||
for(const b of FX.bubbles){const c=CELLS[b.cell];if(!c||c.ph!=='plate')continue;const k=(((fxT-b.d)%2.2)+2.2)%2.2/2.2;circ(o,b.x,b.y-30*k,1.6,COL.bub);}
|
||||
o.globalAlpha=1;
|
||||
// presses: platen colour eases to steam red / cooling blue like the old 0.5 s CSS transition
|
||||
for(const P of PRESS){const m=M[P.id],q=m.plat;if(!q)continue;const ty=m.pY[0]+(m.pY[1]-m.pY[0])*(1-P.open);
|
||||
const h=P.__h||0,c=P.__c||0,C=COL.plat,rgb=[0,1,2].map(i=>Math.round(C.b[i]*(1-h-c)+C.h[i]*h+C.c[i]*c));
|
||||
o.fillStyle='rgb('+rgb+')';o.strokeStyle=C.s;o.lineWidth=1;
|
||||
o.fillRect(q.px,q.yb,q.pw,16);o.strokeRect(q.px,q.yb,q.pw,16);o.fillRect(q.px,ty,q.pw,16);o.strokeRect(q.px,ty,q.pw,16);
|
||||
o.fillStyle='#b9c2cc';o.fillRect(q.px+8,ty+16,q.pw-16,3);if(P.cake)circ(o,q.cx,q.cy,7,'#3d3a33','#8a7a55',1);}
|
||||
let j=0;for(const t of SPR)if(!t.dead)SPR[j++]=t;SPR.length=j;
|
||||
for(const t of SPR){
|
||||
if(t.kind==='dot')circ(o,t.x,t.y,t.r,t.fill,t.stroke,t.sw);
|
||||
else if(t.kind==='rect'){o.fillStyle=t.fill;o.fillRect(t.x-t.w/2,t.y-t.h/2,t.w,t.h);o.lineWidth=1;o.strokeStyle=t.stroke;o.strokeRect(t.x-t.w/2,t.y-t.h/2,t.w,t.h);}
|
||||
else if(t.kind==='rec'){const st=t.cls.rej?['#ff3b3b',2]:t.cls.tp?['#ffbf1f',1.5]:['#3a3f4a',1];circ(o,t.x,t.y,t.r,t.vinyl,st[0],st[1]);circ(o,t.x,t.y,t.lr,t.label);}
|
||||
else for(const [r,fl,st] of SHAPES[t.k]||SHAPES.M)circ(o,t.x,t.y,r,fl,st,1);}}
|
||||
new ResizeObserver(fxResize).observe(SV);
|
||||
new MutationObserver(fxColors).observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});
|
||||
fxResize();fxColors();
|
||||
|
||||
/* ================================================================ seed, then two days of warm-up so the page opens mid-shift */
|
||||
function seedSets(T,used){T.made={A:T.setsTarget,B:T.setsTarget};T.pairs=T.setsTarget;T.mothers={A:{T,side:'A',gen:'M'},B:{T,side:'B',gen:'M'}};
|
||||
for(let k=1;k<=T.setsTarget;k++)RACK.push({T,k,used:k===1?used:0});}
|
||||
@@ -1016,9 +1086,10 @@ let last=performance.now(),tagAcc=0;
|
||||
function frame(now){const dt=Math.min(0.1,(now-last)/1000);last=now;
|
||||
if(S.running){const ds=dt*S.speed;const sub=S.speed>=3600?1:S.speed>=600?0.5:0.25;const n=Math.ceil(ds/sub);const h=ds/n;for(let i=0;i<n;i++)tick(h);}
|
||||
animate(S.running?dt:0);tagAcc+=dt;if(tagAcc>=0.2){tagAcc=0;renderTags();}
|
||||
if(S.running||fxDirty){fxDirty=false;drawFx(S.running?dt:0);}
|
||||
requestAnimationFrame(frame);}
|
||||
requestAnimationFrame(frame);
|
||||
window.__plant={S,TITLES,RACK,PRESS,CELLS,ST,PLATEQ,M,tick};
|
||||
window.__plant={S,TITLES,RACK,PRESS,CELLS,ST,PLATEQ,M,tick,SPR,FX,PATHS,XU,XO};
|
||||
})();
|
||||
(function(){
|
||||
var b=document.getElementById("themeBtn"),de=document.documentElement;
|
||||
|
||||
Reference in new issue
Block a user