refactor: continuous arc+line stroke for seamless gauge (no visible tick gaps)

This commit is contained in:
2026-07-25 17:21:56 +08:00
parent 6b810e3290
commit 9d34f30daf
+86 -77
View File
@@ -14,25 +14,20 @@ html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
#box.cover{min-width:100vw;min-height:100vh} #box.cover{min-width:100vw;min-height:100vh}
#box.fill{width:100vw;height:100vh} #box.fill{width:100vw;height:100vh}
#content{position:relative;width:100%;height:100%;background:#0a0a0f} #content{position:relative;width:100%;height:100%;background:#0a0a0f}
#rpm-area{position:absolute;left:2%;top:4%;width:62%;height:82%} #rpm-area{position:absolute;left:2%;top:4%;width:62%;height:82%}
#rpm-canvas{width:100%;height:100%} #rpm-canvas{width:100%;height:100%}
#speed-area{position:absolute;left:2%;bottom:6%;width:62%;text-align:center} #speed-area{position:absolute;left:2%;bottom:6%;width:62%;text-align:center}
#speed-val{font-size:min(12vw,120px);font-weight:200;line-height:1;letter-spacing:-.02em} #speed-val{font-size:min(12vw,120px);font-weight:200;line-height:1;letter-spacing:-.02em}
#speed-unit{font-size:min(3vw,22px);color:rgba(255,255,255,.2);letter-spacing:.25em;margin-top:4px} #speed-unit{font-size:min(3vw,22px);color:rgba(255,255,255,.2);letter-spacing:.25em;margin-top:4px}
#gear-area{position:absolute;right:5%;top:50%;transform:translateY(-55%)} #gear-area{position:absolute;right:5%;top:50%;transform:translateY(-55%)}
#gear-box{width:min(20vw,170px);height:min(20vw,170px);border-radius:50%;border:1px solid rgba(255,255,255,.1);display:flex;align-items:center;justify-content:center} #gear-box{width:min(20vw,170px);height:min(20vw,170px);border-radius:50%;border:1px solid rgba(255,255,255,.1);display:flex;align-items:center;justify-content:center}
#gear-box span{font-size:min(12vw,100px);font-weight:200;transition:.1s} #gear-box span{font-size:min(12vw,100px);font-weight:200;transition:.1s}
#pedals{position:absolute;right:3%;top:5%;width:20%} #pedals{position:absolute;right:3%;top:5%;width:20%}
.pedal{margin-bottom:16px} .pedal{margin-bottom:16px}
.pedal .pl{font-size:min(1.8vw,13px);color:rgba(255,255,255,.2);margin-bottom:5px;letter-spacing:.15em} .pedal .pl{font-size:min(1.8vw,13px);color:rgba(255,255,255,.2);margin-bottom:5px;letter-spacing:.15em}
.pedal .pb{height:8px;background:rgba(255,255,255,.06);overflow:hidden} .pedal .pb{height:8px;background:rgba(255,255,255,.06);overflow:hidden}
.pedal .pf{height:100%;transition:width .06s linear} .pedal .pf{height:100%;transition:width .06s linear}
#tf{background:#fff}#bf{background:rgba(255,255,255,.4)} #tf{background:#fff}#bf{background:rgba(255,255,255,.4)}
#status{position:absolute;right:4%;bottom:6%;width:20%} #status{position:absolute;right:4%;bottom:6%;width:20%}
.st{margin-bottom:14px} .st{margin-bottom:14px}
.st .sl{font-size:min(1.6vw,12px);color:rgba(255,255,255,.2);margin-bottom:4px;display:flex;justify-content:space-between} .st .sl{font-size:min(1.6vw,12px);color:rgba(255,255,255,.2);margin-bottom:4px;display:flex;justify-content:space-between}
@@ -58,89 +53,103 @@ html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
<script> <script>
const M={aspect_ratio:"16:9",render_mode:"contain"};let GAME_ID=''; const M={aspect_ratio:"16:9",render_mode:"contain"};let GAME_ID='';
(function(){ (function(){
const box=document.getElementById('box'),canvas=document.getElementById('rpm-canvas'), const box=document.getElementById('box'),canvas=document.getElementById('rpm-canvas'),
sV=document.getElementById('speed-val'),sU=document.getElementById('speed-unit'), sV=document.getElementById('speed-val'),sU=document.getElementById('speed-unit'),
gV=document.getElementById('gear-val'),gBox=document.getElementById('gear-box'), gV=document.getElementById('gear-val'),gBox=document.getElementById('gear-box'),
tF=document.getElementById('tf'),bF=document.getElementById('bf'), tF=document.getElementById('tf'),bF=document.getElementById('bf'),
fF=document.getElementById('ff'),fV=document.getElementById('fv'), fF=document.getElementById('ff'),fV=document.getElementById('fv'),
wF=document.getElementById('wf'),wV=document.getElementById('wv'); wF=document.getElementById('wf'),wV=document.getElementById('wv');
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws'; const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
const ctx=canvas.getContext('2d'); const ctx=canvas.getContext('2d');
let ws,rt,lR=-1,lG=-999; let ws,rt,lR=-1,lG=-999;
const Ca=320,Cb=420,Cr=280,As=180,Ae=270;
const N=66,Na=Math.round(N*.5),Nl=N-Na,Ca=320,Cb=420,Cr=280,As=180,Ae=270; function d2r(d){return d*Math.PI/180}
function pr(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2)return +p[0]/+p[1]}
function d2r(d){return d*Math.PI/180} function apply(){
function pr(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2)return +p[0]/+p[1]} const rt=pr(M.aspect_ratio||'16:9'),md=M.render_mode||'contain';
if(!rt){box.style.width='100vw';box.style.height='100vh';box.className='fill';return}
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;
if(md==='cover'){cw=vw;ch=vw/rt;box.className='cover'}
else{box.className='contain';cw=vr>rt?vh*rt:vw;ch=vr>rt?vh:vw/rt}
box.style.width=cw+'px';box.style.height=ch+'px'
}
function apply(){ function sz(){
const rt=pr(M.aspect_ratio||'16:9'),md=M.render_mode||'contain'; const r=canvas.parentElement.getBoundingClientRect(),d=window.devicePixelRatio||1;
if(!rt){box.style.width='100vw';box.style.height='100vh';box.className='fill';return} canvas.width=r.width*d;canvas.height=r.height*d;
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch; canvas.style.width=r.width+'px';canvas.style.height=r.height+'px';
if(md==='cover'){cw=vw;ch=vw/rt;box.className='cover'} ctx.setTransform(d,0,0,d,0,0);
else{box.className='contain';cw=vr>rt?vh*rt:vw;ch=vr>rt?vh:vw/rt} }
box.style.width=cw+'px';box.style.height=ch+'px'
function draw(cur,max){
max=max||8000;cur=cur||0;sz();
const W=canvas.width/devicePixelRatio,H=canvas.height/devicePixelRatio;
ctx.clearRect(0,0,W,H);
const sx=Ca/640*W,sy=Cb/480*H,sr=Cr/640*W;
const gw=Math.max(36,W/18);
const ae=d2r(Ae),ex=sx+sr*Math.cos(ae),ey=sy+sr*Math.sin(ae),lx=W*.93;
const arcLen=Math.PI*2*sr*((Ae-As)/360),lineLen=lx-ex,totalLen=arcLen+lineLen;
const rl=max*.85;
// background stroke
ctx.beginPath();
ctx.arc(sx,sy,sr,d2r(As),d2r(Ae),true);
ctx.lineTo(lx,ey);
ctx.strokeStyle='rgba(255,255,255,.04)';
ctx.lineWidth=gw;ctx.lineCap='round';ctx.lineJoin='round';
ctx.stroke();
// progress stroke
const pc=Math.min(cur/max,1);
if(pc>0){
const pd=pc*totalLen;
ctx.beginPath();
if(pd<=arcLen){
ctx.arc(sx,sy,sr,d2r(As),d2r(As+(Ae-As)*(pd/arcLen)),true);
}else{
ctx.arc(sx,sy,sr,d2r(As),d2r(Ae),true);
ctx.lineTo(ex+(pd-arcLen)/lineLen*(lx-ex),ey);
}
ctx.strokeStyle=cur>=rl?'rgba(255,50,35,.95)':'rgba(255,255,255,.85)';
ctx.lineWidth=gw;ctx.lineCap='round';ctx.lineJoin='round';
ctx.stroke();
} }
function sz(){ // scale marks
const r=canvas.parentElement.getBoundingClientRect(),d=window.devicePixelRatio||1; const mr=sr-gw/2-6,pr=sr+gw/2+6;
canvas.width=r.width*d;canvas.height=r.height*d; for(let r=0;r<=max;r+=2000){
canvas.style.width=r.width+'px';canvas.style.height=r.height+'px'; const a=d2r(As+(Ae-As)*(r/max));
ctx.setTransform(d,0,0,d,0,0); ctx.beginPath();ctx.moveTo(sx+mr*Math.cos(a),sy+mr*Math.sin(a));ctx.lineTo(sx+pr*Math.cos(a),sy+pr*Math.sin(a));
ctx.strokeStyle=r>=rl?'rgba(255,255,255,.15)':'rgba(255,255,255,.08)';
ctx.lineWidth=r%4000===0?2.5:1;ctx.stroke();
if(r%4000===0){ctx.fillStyle='rgba(255,255,255,.22)';ctx.font=`${Math.max(13,W/38)|0}px sans-serif`;ctx.textAlign='center';ctx.fillText(Math.round(r/1000),sx+(pr+22)*Math.cos(a),sy+(pr+22)*Math.sin(a)+5)}
} }
function tp(i){ // RPM number
const W=canvas.width/devicePixelRatio,H=canvas.height/devicePixelRatio; ctx.fillStyle='#fff';ctx.font=`200 ${Math.max(48,W/12)|0}px sans-serif`;ctx.textAlign='center';
const sx=Ca/640*W,sy=Cb/480*H,sr=Cr/640*W; ctx.fillText(Math.round(cur).toLocaleString(),sx,sy-180);
if(i<Na){const p=i/(Na-1),a=d2r(As+(Ae-As)*p);return{x:sx+sr*Math.cos(a),y:sy+sr*Math.sin(a),r:a-Math.PI/2}} ctx.fillStyle='rgba(255,255,255,.12)';ctx.font=`${Math.max(12,W/44)|0}px sans-serif`;
const ae=d2r(Ae),ex=sx+sr*Math.cos(ae),ey=sy+sr*Math.sin(ae),p=(i-Na+1)/(Nl-1); ctx.fillText('\u00d71000 rpm',sx,sy-155);
return{x:ex+p*(W*.93-ex),y:ey,r:0}
}
function draw(cur,max){ // redline
max=max||8000;cur=cur||0;sz(); ctx.strokeStyle='rgba(255,40,30,.1)';ctx.lineWidth=1;ctx.setLineDash([3,6]);
const W=canvas.width/devicePixelRatio,H=canvas.height/devicePixelRatio; ctx.beginPath();ctx.moveTo(sx,sy);ctx.lineTo(sx+sr*Math.cos(d2r(As+(Ae-As)*.85)),sy+sr*Math.sin(d2r(As+(Ae-As)*.85)));ctx.stroke();ctx.setLineDash([])
ctx.clearRect(0,0,W,H); }
const tw=Math.max(108,W/5),th=Math.max(36,H/12),gap=Math.max(3,tw*.04);
// tick thickness varies: 2x at bottom arc, 1x at top function cn(){ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}}
function twScale(i){if(i>=Na)return 1;return 2-(i/(Na-1))} ws.onmessage=(e)=>{try{const m=JSON.parse(e.data);if(m.type==='connected')GAME_ID=m.selected_game_id||'';if(m.type==='telemetry')up(m.data)}catch(e){}}
ws.onclose=()=>{rt=setTimeout(cn,2000)}}
// bg ticks function up(d){const rpm=d.rpm||0,mr=d.max_rpm||8000,g=d.gear||0,sp=d.speed_kmh||0;
for(let i=0;i<N;i++){const p=tp(i),s=twScale(i),w=(tw-gap)*s,h=th*s;ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.r);ctx.beginPath();ctx.roundRect(-w/2,-h/2,w,h,th*.4);ctx.fillStyle='rgba(255,255,255,.04)';ctx.fill();ctx.restore()} if(Math.abs(rpm-lR)>25||g!==lG){lR=rpm;lG=g;draw(rpm,mr)}
sV.textContent=Math.round(sp);gV.textContent=g===0?'N':g===11?'R':g===21?'P':String(g);
tF.style.width=(d.throttle*100).toFixed(0)+'%';bF.style.width=(d.brake*100).toFixed(0)+'%';
const fp=Math.round((d.fuel||1)*100);fF.style.width=fp+'%';fV.textContent=fp+'%';
const et=d.engine_temp||0;if(et>0){const ep=Math.min(et/120,1)*100;wF.style.width=ep+'%';wV.textContent=Math.round(et)+'\u00B0C'}}
// progress draw(0,8000);apply();window.addEventListener('resize',()=>{apply();draw(lR>0?lR:0,8000)});cn()
const pc=Math.min(cur/max,1),on=Math.round(pc*N),rl=max*.85;
for(let i=0;i<on;i++){const p=tp(i),s=twScale(i),w=(tw-gap)*s,h=th*s;ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.r);ctx.beginPath();ctx.roundRect(-w/2,-h/2,w,h,th*.4);ctx.fillStyle=cur>=rl?'rgba(255,50,35,.95)':'rgba(255,255,255,.85)';ctx.fill();ctx.restore()}
// scale nums - larger
const sx=Ca/640*W,sy=Cb/480*H,sr=Cr/640*W;
ctx.fillStyle='rgba(255,255,255,.2)';ctx.font=`${Math.max(12,W/42)|0}px sans-serif`;ctx.textAlign='center';
for(let r=0;r<=max;r+=2000){const p=r/max,a=d2r(As+(Ae-As)*p);ctx.fillText(Math.round(r/1000),sx+(sr-54)*Math.cos(a),sy+(sr-54)*Math.sin(a)+4)}
// RPM number - big, high up
ctx.fillStyle='#fff';ctx.font=`200 ${Math.max(48,W/12)|0}px sans-serif`;ctx.textAlign='center';
ctx.fillText(Math.round(cur).toLocaleString(),sx,sy-180);
ctx.fillStyle='rgba(255,255,255,.12)';ctx.font=`${Math.max(12,W/44)|0}px sans-serif`;
ctx.fillText('\u00d71000 rpm',sx,sy-155);
// redline
ctx.strokeStyle='rgba(255,40,30,.1)';ctx.lineWidth=1;ctx.setLineDash([3,6]);ctx.beginPath();
const ra=d2r(As+(Ae-As)*.85);ctx.moveTo(sx,sy);ctx.lineTo(sx+sr*Math.cos(ra),sy+sr*Math.sin(ra));ctx.stroke();ctx.setLineDash([])
}
function cn(){ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}}
ws.onmessage=(e)=>{try{const m=JSON.parse(e.data);if(m.type==='connected')GAME_ID=m.selected_game_id||'';if(m.type==='telemetry')up(m.data)}catch(e){}}
ws.onclose=()=>{rt=setTimeout(cn,2000)}}
function up(d){const rpm=d.rpm||0,mr=d.max_rpm||8000,g=d.gear||0,sp=d.speed_kmh||0;
if(Math.abs(rpm-lR)>25||g!==lG){lR=rpm;lG=g;draw(rpm,mr)}
sV.textContent=Math.round(sp);gV.textContent=g===0?'N':g===11?'R':g===21?'P':String(g);
tF.style.width=(d.throttle*100).toFixed(0)+'%';bF.style.width=(d.brake*100).toFixed(0)+'%';
const fp=Math.round((d.fuel||1)*100);fF.style.width=fp+'%';fV.textContent=fp+'%';
const et=d.engine_temp||0;if(et>0){const ep=Math.min(et/120,1)*100;wF.style.width=ep+'%';wV.textContent=Math.round(et)+'\u00B0C'}}
draw(0,8000);apply();window.addEventListener('resize',()=>{apply();draw(lR>0?lR:0,8000)});cn()
})(); })();
</script> </script>
</body> </body>