9091f2133d
- Scene editor: drag-drop dashboards from sidebar to canvas, reposition, delete placements, save/load - Systemd service: turbosu.service with frpc dependency, restart on failure - F1 composite: speed/RPM/lap/gear/throttle/brake/fuel/DRS, 16:9, NEXT ART font
178 lines
7.3 KiB
HTML
178 lines
7.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
|
<title>TurboSu - F1 Composite</title>
|
|
<style>
|
|
@font-face{font-family:'NEXT ART';src:url('/static/fonts/NEXT_ART_Heavy.otf') format('opentype')}
|
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
|
font-family:'NEXT ART',monospace}
|
|
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
|
#box{position:relative;overflow:hidden}
|
|
#box.contain{max-width:100vw;max-height:100vh}
|
|
#box.cover{min-width:100vw;min-height:100vh}
|
|
#box.fill{width:100vw;height:100vh}
|
|
#content{position:relative;width:100%;height:100%;background:#0a0a0f}
|
|
|
|
#drs{position:absolute;right:4%;top:4%;padding:6px 18px;
|
|
border:1px solid rgba(255,255,255,.08);border-radius:4px;
|
|
font-size:min(1.6vw,24px);color:rgba(255,255,255,.15);letter-spacing:.3em;opacity:.5}
|
|
|
|
#left-col{position:absolute;left:4%;top:10%;width:58%}
|
|
#speed-val{font-size:min(16vw,300px);font-weight:200;line-height:.85;letter-spacing:-.02em}
|
|
#speed-unit{font-size:min(2.5vw,36px);color:rgba(255,255,255,.15);letter-spacing:.35em;margin-top:4px}
|
|
|
|
#rpm-row{display:flex;align-items:baseline;gap:12px;margin-top:1.5vh}
|
|
#rpm-val{font-size:min(8vw,140px);font-weight:200;line-height:1;letter-spacing:-.02em}
|
|
#rpm-unit{font-size:min(1.8vw,26px);color:rgba(255,255,255,.15);letter-spacing:.2em}
|
|
|
|
#lap-area{position:absolute;left:4%;top:58%;display:flex;align-items:flex-end;gap:min(4vw,60px)}
|
|
#lap-box{text-align:center}
|
|
#lap-label{font-size:min(1.4vw,20px);color:rgba(255,255,255,.15);letter-spacing:.25em;margin-bottom:2px}
|
|
#lap-num{font-size:min(5vw,72px);font-weight:200;line-height:1;letter-spacing:-.02em}
|
|
#lap-time{font-size:min(4vw,56px);font-weight:200;font-variant-numeric:tabular-nums;letter-spacing:-.02em;color:rgba(255,255,255,.6)}
|
|
#best-box{text-align:center}
|
|
#best-label{font-size:min(1.3vw,18px);color:rgba(255,255,255,.1);letter-spacing:.25em;margin-bottom:2px}
|
|
#best-time{font-size:min(2.8vw,40px);font-weight:200;font-variant-numeric:tabular-nums;color:rgba(255,255,255,.25)}
|
|
|
|
#gear-area{position:absolute;left:50%;transform:translateX(-50%);bottom:8%}
|
|
#gear-box{width:min(20vw,260px);height:min(20vw,260px);border-radius:50%;
|
|
border:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:center;
|
|
background:rgba(255,255,255,.02)}
|
|
#gear-val{font-size:min(14vw,220px);font-weight:200;line-height:1;transition:color .15s}
|
|
|
|
#right-col{position:absolute;right:4%;top:14%;width:20%}
|
|
.pedal{margin-bottom:3vh}
|
|
.pedal .pl{font-size:min(1.4vw,22px);color:rgba(255,255,255,.2);margin-bottom:6px;letter-spacing:.25em}
|
|
.pedal .pb{height:min(1.4vw,22px);background:rgba(255,255,255,.04);overflow:hidden}
|
|
.pedal .pf{height:100%;transition:width .08s linear}
|
|
#tf{background:rgba(255,255,255,.75)}#bf{background:rgba(255,255,255,.3)}
|
|
|
|
#fuel-box{margin-top:6vh}
|
|
#fuel-label{font-size:min(1.4vw,22px);color:rgba(255,255,255,.2);margin-bottom:6px;display:flex;justify-content:space-between;letter-spacing:.25em}
|
|
#fuel-label span:last-child{color:rgba(255,255,255,.35)}
|
|
#fuel-bar{height:min(1.4vw,22px);background:rgba(255,255,255,.04);overflow:hidden}
|
|
#fuel-fill{height:100%;background:rgba(255,255,255,.15);transition:width .5s}
|
|
|
|
#accel{position:absolute;right:28%;top:4%;text-align:right}
|
|
#accel div{font-size:min(1vw,15px);color:rgba(255,255,255,.08);letter-spacing:.15em;font-variant-numeric:tabular-nums}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="root"><div id="box" class="contain"><div id="content">
|
|
<div id="drs">DRS</div>
|
|
|
|
<div id="left-col">
|
|
<div id="speed-val">0</div>
|
|
<div id="speed-unit">KM/H</div>
|
|
<div id="rpm-row">
|
|
<span id="rpm-val">0</span>
|
|
<span id="rpm-unit">RPM</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="lap-area">
|
|
<div id="lap-box">
|
|
<div id="lap-label">LAP</div>
|
|
<div id="lap-num">0</div>
|
|
</div>
|
|
<div>
|
|
<div id="lap-time">0:00.0</div>
|
|
</div>
|
|
<div id="best-box">
|
|
<div id="best-label">BEST</div>
|
|
<div id="best-time">--:--.-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="gear-area"><div id="gear-box"><span id="gear-val">N</span></div></div>
|
|
|
|
<div id="right-col">
|
|
<div class="pedal"><div class="pl">THR</div><div class="pb"><div class="pf" id="tf" style="width:0%"></div></div></div>
|
|
<div class="pedal"><div class="pl">BRK</div><div class="pb"><div class="pf" id="bf" style="width:0%"></div></div></div>
|
|
<div id="fuel-box">
|
|
<div id="fuel-label"><span>FUEL</span><span id="fuel-pct">100%</span></div>
|
|
<div id="fuel-bar"><div id="fuel-fill" style="width:100%"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="accel">
|
|
<div>G-LAT <span id="ax">0.00</span></div>
|
|
<div>G-LON <span id="az">0.00</span></div>
|
|
</div>
|
|
</div></div></div>
|
|
|
|
<script>
|
|
const M={aspect_ratio:"16:9",render_mode:"contain"};
|
|
(function(){
|
|
const box=document.getElementById('box'),sV=document.getElementById('speed-val'),
|
|
rV=document.getElementById('rpm-val'),gV=document.getElementById('gear-val'),
|
|
tF=document.getElementById('tf'),bF=document.getElementById('bf'),
|
|
fF=document.getElementById('fuel-fill'),fP=document.getElementById('fuel-pct'),
|
|
lN=document.getElementById('lap-num'),lT=document.getElementById('lap-time'),
|
|
bT=document.getElementById('best-time'),gBox=document.getElementById('gear-box'),
|
|
aX=document.getElementById('ax'),aZ=document.getElementById('az');
|
|
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
|
let ws,rt,lR=-1,lG=-999;
|
|
|
|
function fmt(t){if(!t||t<=0)return'0:00.0';const m=Math.floor(t/60),s=(t%60).toFixed(1);return m+':'+s.padStart(4,'0')}
|
|
function fmtB(t){if(!t||t<=0)return'--:--.-';return fmt(t)}
|
|
|
|
function pr(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2)return +p[0]/+p[1]}
|
|
|
|
function apply(){
|
|
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 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==='telemetry')up(m.data)}catch(e){}}
|
|
ws.onclose=()=>{rt=setTimeout(cn,2000)}}
|
|
|
|
function gearStr(g){
|
|
if(g===0||!g)return'N';
|
|
if(g>=1&&g<=8)return String(g);
|
|
return String(g);
|
|
}
|
|
|
|
function up(d){
|
|
const rpm=d.rpm||0,mr=d.max_rpm||15000,g=d.gear||0;
|
|
|
|
sV.textContent=Math.round(d.speed_kmh||0);
|
|
rV.textContent=Math.round(rpm);
|
|
|
|
if(Math.abs(rpm-lR)>50||g!==lG){
|
|
lR=rpm;lG=g;
|
|
const pct=mr>0?Math.min(rpm/mr,1):0;
|
|
gBox.style.borderColor=pct>.9?'rgba(255,50,35,.5)':'rgba(255,255,255,.08)';
|
|
gV.style.color=pct>.9?'rgba(255,50,35,.95)':'#fff';
|
|
}
|
|
|
|
gV.textContent=gearStr(g);
|
|
tF.style.width=((d.throttle||0)*100).toFixed(0)+'%';
|
|
bF.style.width=((d.brake||0)*100).toFixed(0)+'%';
|
|
|
|
const fp=Math.round((d.fuel||1)*100);
|
|
fF.style.width=fp+'%';fP.textContent=fp+'%';
|
|
|
|
lN.textContent=d.lap_number||0;
|
|
lT.textContent=fmt(d.lap_time);
|
|
bT.textContent=fmtB(d.best_lap);
|
|
|
|
aX.textContent=(d.acceleration_x||0).toFixed(2);
|
|
aZ.textContent=(d.acceleration_z||0).toFixed(2);
|
|
}
|
|
|
|
apply();window.addEventListener('resize',()=>{apply()});cn();
|
|
setTimeout(()=>{apply()},100);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|