style: simplify all dashboards to minimal flat design, remove Miuix decorations
This commit is contained in:
@@ -6,251 +6,135 @@
|
||||
<title>TurboSu - Forza Composite</title>
|
||||
<style>
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;
|
||||
font-family:'SF Pro Text',-apple-system,'PingFang SC','MiSans',sans-serif;
|
||||
color:#fff;-webkit-tap-highlight-color:transparent}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
||||
font-family:'SF Pro Display',-apple-system,system-ui,sans-serif}
|
||||
#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}
|
||||
#box.center{width:auto;height:auto}
|
||||
#content{position:relative;width:100%;height:100%;background:linear-gradient(165deg,#050510 0%,#0a0a1e 50%,#0d0d28 100%)}
|
||||
#content{position:relative;width:100%;height:100%;background:#0a0a0f}
|
||||
|
||||
/* ---- RPM Arc (left-center) ---- */
|
||||
#rpm-arc{position:absolute;left:4%;top:8%;width:58%;height:78%}
|
||||
#rpm-arc text{fill:rgba(255,255,255,.9)}
|
||||
/* RPM arc */
|
||||
#rpm-area{position:absolute;left:3%;top:6%;width:60%;height:78%}
|
||||
#rpm-svg{width:100%;height:100%}
|
||||
|
||||
/* ---- Speed (below RPM arc) ---- */
|
||||
#speed-area{position:absolute;left:4%;bottom:10%;width:58%;text-align:center}
|
||||
#speed-val{font-size:min(7vw,72px);font-weight:900;line-height:1;text-shadow:0 0 40px rgba(52,130,255,.4)}
|
||||
#speed-unit{font-size:min(2vw,18px);color:rgba(255,255,255,.4);letter-spacing:.15em;margin-top:2px}
|
||||
/* Speed */
|
||||
#speed-area{position:absolute;left:3%;bottom:8%;width:60%;text-align:center}
|
||||
#speed-val{font-size:min(8vw,76px);font-weight:200;line-height:1;letter-spacing:-.02em}
|
||||
#speed-unit{font-size:min(2vw,16px);color:rgba(255,255,255,.2);letter-spacing:.2em;margin-top:2px}
|
||||
|
||||
/* ---- Gear (right side, large) ---- */
|
||||
#gear-box{position:absolute;right:8%;top:50%;transform:translateY(-55%);
|
||||
width:min(18vw,160px);height:min(18vw,160px);
|
||||
border-radius:50%;display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(52,130,255,.08);border:3px solid rgba(52,130,255,.3);
|
||||
box-shadow:0 0 60px rgba(52,130,255,.1)}
|
||||
#gear-val{font-size:min(9vw,80px);font-weight:900;transition:transform .12s ease}
|
||||
#gear-val.flash{color:#FF9F0A;transform:scale(1.15)}
|
||||
/* Gear */
|
||||
#gear-area{position:absolute;right:6%;top:50%;transform:translateY(-55%);text-align:center}
|
||||
#gear-box{width:min(16vw,140px);height:min(16vw,140px);border-radius:50%;border:2px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center}
|
||||
#gear-box span{font-size:min(8vw,72px);font-weight:200;transition:.1s}
|
||||
|
||||
/* ---- Throttle / Brake (top-right) ---- */
|
||||
#pedal-area{position:absolute;right:4%;top:6%;width:20%;display:flex;flex-direction:column;gap:14px}
|
||||
.pedal-row{display:flex;align-items:center;gap:8px}
|
||||
.pedal-label{font-size:min(1.4vw,11px);color:rgba(255,255,255,.45);width:28px;text-align:right}
|
||||
.pedal-bar{flex:1;height:10px;background:rgba(255,255,255,.08);border-radius:5px;overflow:hidden}
|
||||
.pedal-fill{height:100%;border-radius:5px;transition:width .08s linear}
|
||||
#throttle-fill{background:linear-gradient(90deg,#3482FF,#5D9BFF)}
|
||||
#brake-fill{background:linear-gradient(90deg,#E94634,#FF6B5B)}
|
||||
/* Pedals */
|
||||
#pedals{position:absolute;right:4%;top:5%;width:18%}
|
||||
.pedal{margin-bottom:12px}
|
||||
.pedal .pl{font-size:min(1.3vw,10px);color:rgba(255,255,255,.2);margin-bottom:4px;text-transform:uppercase;letter-spacing:.1em}
|
||||
.pedal .pb{height:6px;background:rgba(255,255,255,.06);overflow:hidden}
|
||||
.pedal .pf{height:100%;transition:width .06s linear}
|
||||
#tf{background:#fff}#bf{background:rgba(255,255,255,.4)}
|
||||
|
||||
/* ---- Fuel / Water Temp (bottom-right) ---- */
|
||||
#status-area{position:absolute;right:4%;bottom:8%;width:20%;display:flex;flex-direction:column;gap:16px}
|
||||
.status-row{display:flex;align-items:center;gap:8px}
|
||||
.status-icon{font-size:min(1.6vw,14px);width:22px;text-align:center}
|
||||
.status-label{font-size:min(1.2vw,10px);color:rgba(255,255,255,.35);width:24px}
|
||||
.status-bar{flex:1;height:6px;background:rgba(255,255,255,.08);border-radius:3px;overflow:hidden}
|
||||
.status-fill{height:100%;border-radius:3px;transition:width .3s ease}
|
||||
#fuel-fill{background:linear-gradient(90deg,#FF9F0A,#F5A623)}
|
||||
#water-fill{background:linear-gradient(90deg,#34C759,#5DD875)}
|
||||
.status-val{font-size:min(1.2vw,10px);color:rgba(255,255,255,.4);width:32px;text-align:right}
|
||||
|
||||
/* ---- boost indicator ---- */
|
||||
#boost-area{position:absolute;right:4%;top:35%;width:20%}
|
||||
#boost-val{font-size:min(3vw,24px);font-weight:700;color:#a78bfa}
|
||||
|
||||
/* ---- RPM scale marks ---- */
|
||||
.rpm-mark{stroke:rgba(255,255,255,.15);stroke-width:1}
|
||||
.rpm-mark-major{stroke:rgba(255,255,255,.4);stroke-width:2}
|
||||
.rpm-mark-red{stroke:rgba(233,70,52,.5);stroke-width:1}
|
||||
|
||||
/* ---- scale text ---- */
|
||||
#scale-text{position:absolute;left:8%;top:16%;font-size:min(1.2vw,11px);color:rgba(255,255,255,.25)}
|
||||
/* Fuel / Temp */
|
||||
#status{position:absolute;right:4%;bottom:6%;width:18%}
|
||||
.st{margin-bottom:10px}
|
||||
.st .sl{font-size:min(1.2vw,10px);color:rgba(255,255,255,.2);margin-bottom:3px;display:flex;justify-content:space-between}
|
||||
.st .sb{height:3px;background:rgba(255,255,255,.06);overflow:hidden}
|
||||
.st .sf{height:100%;transition:width .3s}
|
||||
#ff{background:rgba(255,255,255,.3)}#wf{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="box" class="contain"><div id="content">
|
||||
|
||||
<!-- RPM Arc -->
|
||||
<svg id="rpm-arc" viewBox="0 0 600 400" preserveAspectRatio="xMidYMid meet"></svg>
|
||||
<div id="scale-text">×1000 rpm</div>
|
||||
|
||||
<!-- Speed -->
|
||||
<div id="speed-area">
|
||||
<div id="speed-val">0</div>
|
||||
<div id="speed-unit">KM/H</div>
|
||||
<div id="rpm-area"><svg id="rpm-svg" viewBox="0 0 640 440" preserveAspectRatio="xMidYMid meet"></svg></div>
|
||||
<div id="speed-area"><div id="speed-val">0</div><div id="speed-unit">KM/H</div></div>
|
||||
<div id="gear-area"><div id="gear-box"><span id="gear-val">1</span></div></div>
|
||||
<div id="pedals">
|
||||
<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>
|
||||
|
||||
<!-- Gear -->
|
||||
<div id="gear-box"><span id="gear-val">1</span></div>
|
||||
|
||||
<!-- Pedals (throttle/brake) -->
|
||||
<div id="pedal-area">
|
||||
<div class="pedal-row">
|
||||
<span class="pedal-label">油门</span>
|
||||
<div class="pedal-bar"><div class="pedal-fill" id="throttle-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div class="pedal-row">
|
||||
<span class="pedal-label">刹车</span>
|
||||
<div class="pedal-bar"><div class="pedal-fill" id="brake-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="status">
|
||||
<div class="st"><div class="sl"><span>FUEL</span><span id="fv">100%</span></div><div class="sb"><div class="sf" id="ff" style="width:100%"></div></div></div>
|
||||
<div class="st"><div class="sl"><span>TEMP</span><span id="wv">--</span></div><div class="sb"><div class="sf" id="wf" style="width:20%"></div></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Fuel / Water -->
|
||||
<div id="status-area">
|
||||
<div class="status-row">
|
||||
<span class="status-icon">⛽</span>
|
||||
<span class="status-label">FUEL</span>
|
||||
<div class="status-bar"><div class="status-fill" id="fuel-fill" style="width:100%"></div></div>
|
||||
<span class="status-val" id="fuel-val">100%</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-icon">🌡</span>
|
||||
<span class="status-label">TEMP</span>
|
||||
<div class="status-bar"><div class="status-fill" id="water-fill" style="width:30%"></div></div>
|
||||
<span class="status-val" id="water-val">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"16:9",render_mode:"contain"};
|
||||
let GAME_ID='';
|
||||
|
||||
(function(){
|
||||
const box=document.getElementById('box'),arcSvg=document.getElementById('rpm-arc'),
|
||||
speedV=document.getElementById('speed-val'),speedU=document.getElementById('speed-unit'),
|
||||
gearV=document.getElementById('gear-val'),gearBox=document.getElementById('gear-box'),
|
||||
tFill=document.getElementById('throttle-fill'),bFill=document.getElementById('brake-fill'),
|
||||
fFill=document.getElementById('fuel-fill'),fVal=document.getElementById('fuel-val'),
|
||||
wFill=document.getElementById('water-fill'),wVal=document.getElementById('water-val');
|
||||
const box=document.getElementById('box'),svg=document.getElementById('rpm-svg'),
|
||||
sV=document.getElementById('speed-val'),sU=document.getElementById('speed-unit'),
|
||||
gV=document.getElementById('gear-val'),gBox=document.getElementById('gear-box'),
|
||||
tF=document.getElementById('tf'),bF=document.getElementById('bf'),
|
||||
fF=document.getElementById('ff'),fV=document.getElementById('fv'),
|
||||
wF=document.getElementById('wf'),wV=document.getElementById('wv');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt,lastGear=-999,lastRpm=-1;
|
||||
let ws,rt,lR=-1,lG=-999;
|
||||
|
||||
// Arc geometry (in viewBox 600x400 coords)
|
||||
const CX=300,CY=340,R=260;
|
||||
const ARC_START=-205,ARC_END=-25; // degrees, from -205° (bottom-left) to -25° (bottom-right)
|
||||
const CX=320,CY=380,R=290,SA=-210,EA=-30;
|
||||
|
||||
function degToRad(d){return d*Math.PI/180}
|
||||
function d2r(d){return d*Math.PI/180}
|
||||
function arcP(s,e,r){const sa=d2r(s),ea=d2r(e);return`M${CX+r*Math.cos(sa)} ${CY+r*Math.sin(sa)} A${r} ${r} 0 ${e-s>180?1:0} 1 ${CX+r*Math.cos(ea)} ${CY+r*Math.sin(ea)}`}
|
||||
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];return w/h}return null}
|
||||
|
||||
function apply(){
|
||||
const ratio=parseRatio(M.aspect_ratio||'16:9'),mode=M.render_mode||'contain';
|
||||
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
|
||||
if(!ratio){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;
|
||||
switch(mode){
|
||||
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
|
||||
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
|
||||
case'center':cw=Math.min(vw,1920);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,1080);cw=ch*ratio};box.className='center';break;
|
||||
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
|
||||
}
|
||||
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
|
||||
drawArc();
|
||||
if(mode==='cover'){cw=vw;ch=vw/ratio;box.className='cover'}
|
||||
else{box.className='contain';cw=vr>ratio?vh*ratio:vw;ch=vr>ratio?vh:vw/ratio}
|
||||
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||
}
|
||||
|
||||
function drawArc(maxRpm,curRpm,gear){
|
||||
maxRpm=maxRpm||8000;curRpm=curRpm||0;
|
||||
const redline=Math.max(maxRpm*0.85,maxRpm-500);
|
||||
|
||||
let svg='';
|
||||
// arc background
|
||||
svg+=`<path d="${arcPath(ARC_START,ARC_END,R)}" fill="none" stroke="rgba(255,255,255,.06)" stroke-width="28" stroke-linecap="round"/>`;
|
||||
|
||||
// gear segment demarcations (every 1000 rpm)
|
||||
const totalAngle=ARC_END-ARC_START;
|
||||
for(let r=0;r<=maxRpm;r+=1000){
|
||||
const pct=r/maxRpm;
|
||||
const ang=ARC_START+totalAngle*pct;
|
||||
const a=degToRad(ang);
|
||||
const x1=CX+(R-24)*Math.cos(a),y1=CY+(R-24)*Math.sin(a);
|
||||
const x2=CX+(R+24)*Math.cos(a),y2=CY+(R+24)*Math.sin(a);
|
||||
const color=r>=redline?'rgba(233,70,52,.4)':(r%2000===0?'rgba(255,255,255,.35)':'rgba(255,255,255,.12)');
|
||||
const sw=r%2000===0?2:1;
|
||||
svg+=`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${color}" stroke-width="${sw}"/>`;
|
||||
if(r%2000===0){
|
||||
const lx=CX+(R-44)*Math.cos(a),ly=CY+(R-44)*Math.sin(a);
|
||||
svg+=`<text x="${lx}" y="${ly}" text-anchor="middle" font-size="11" fill="rgba(255,255,255,.35)">${r/1000}</text>`;
|
||||
function drawRpm(cur,max,gear){
|
||||
max=max||8000;cur=cur||0;
|
||||
const ta=EA-SA,rd=max*0.85;
|
||||
let h='';
|
||||
h+=`<path d="${arcP(SA,EA,R)}" fill="none" stroke="rgba(255,255,255,.04)" stroke-width="22" stroke-linecap="round"/>`;
|
||||
for(let r=0;r<=max;r+=1000){
|
||||
const pct=r/max,ang=SA+ta*pct,a=d2r(ang);
|
||||
const x1=CX+(R-20)*Math.cos(a),y1=CY+(R-20)*Math.sin(a);
|
||||
const x2=CX+(R+20)*Math.cos(a),y2=CY+(R+20)*Math.sin(a);
|
||||
const c=r>=rd?'rgba(255,255,255,.08)':(r%2000===0?'rgba(255,255,255,.12)':'rgba(255,255,255,.04)');
|
||||
h+=`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${c}" stroke-width="${r%2000===0?2:1}"/>`;
|
||||
if(r%2000===0)h+=`<text x="${CX+(R-36)*Math.cos(a)}" y="${CY+(R-36)*Math.sin(a)+4}" text-anchor="middle" font-size="10" fill="rgba(255,255,255,.2)">${r/1000}</text>`;
|
||||
}
|
||||
if(cur>0){
|
||||
const pct=Math.min(cur/max,1),eA=SA+ta*pct;
|
||||
h+=`<path d="${arcP(SA,eA,R)}" fill="none" stroke="#fff" stroke-width="22" stroke-linecap="round"/>`;
|
||||
}
|
||||
|
||||
// RPM filled arc
|
||||
if(curRpm>0){
|
||||
const pct=Math.min(curRpm/maxRpm,1);
|
||||
const endAng=ARC_START+totalAngle*pct;
|
||||
const color=curRpm>=redline?'#E94634':(curRpm>=redline*0.7?'#FF9F0A':'#3482FF');
|
||||
svg+=`<path d="${arcPath(ARC_START,endAng,R)}" fill="none" stroke="${color}" stroke-width="28" stroke-linecap="round"/>`;
|
||||
svg+=`<filter id="glow"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>`;
|
||||
svg+=`<path d="${arcPath(endAng-1,endAng,R)}" fill="none" stroke="${color}" stroke-width="32" stroke-linecap="round" filter="url(#glow)" opacity=".6"/>`;
|
||||
}
|
||||
|
||||
// RPM numeric value center
|
||||
svg+=`<text x="${CX}" y="${CY-8}" text-anchor="middle" font-size="28" font-weight="700" fill="#fff">${Math.round(curRpm).toLocaleString()}</text>`;
|
||||
svg+=`<text x="${CX}" y="${CY+14}" text-anchor="middle" font-size="12" fill="rgba(255,255,255,.35)">RPM</text>`;
|
||||
|
||||
arcSvg.innerHTML=svg;
|
||||
}
|
||||
|
||||
function arcPath(startDeg,endDeg,r){
|
||||
const sa=degToRad(startDeg),ea=degToRad(endDeg);
|
||||
const x1=CX+r*Math.cos(sa),y1=CY+r*Math.sin(sa);
|
||||
const x2=CX+r*Math.cos(ea),y2=CY+r*Math.sin(ea);
|
||||
const sweep=endDeg-startDeg>180?1:0;
|
||||
return `M ${x1} ${y1} A ${r} ${r} 0 ${sweep} 1 ${x2} ${y2}`;
|
||||
h+=`<text x="${CX}" y="${CY-12}" text-anchor="middle" font-size="24" font-weight="200" fill="#fff">${Math.round(cur).toLocaleString()}</text>`;
|
||||
h+=`<text x="${CX}" y="${CY+14}" text-anchor="middle" font-size="11" fill="rgba(255,255,255,.2)">×1000 rpm</text>`;
|
||||
svg.innerHTML=h;
|
||||
}
|
||||
|
||||
function connect(){
|
||||
ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}};
|
||||
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==='connected')GAME_ID=m.selected_game_id||'';
|
||||
if(m.type==='telemetry')update(m.data)
|
||||
}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
}catch(e){}}
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)}
|
||||
}
|
||||
|
||||
function update(d){
|
||||
const rpm=d.rpm||0,maxRpm=d.max_rpm||8000,gear=d.gear||0;
|
||||
const speed=d.speed_kmh||0;
|
||||
|
||||
// RPM arc
|
||||
if(Math.abs(rpm-lastRpm)>50||gear!==lastGear){
|
||||
lastRpm=rpm;lastGear=gear;
|
||||
drawArc(maxRpm,rpm,gear);
|
||||
}
|
||||
|
||||
// Speed
|
||||
speedV.textContent=Math.round(speed);
|
||||
speedU.textContent='KM/H';
|
||||
|
||||
// Gear
|
||||
const gv=gear===0?'N':gear===11?'R':gear===21?'P':String(gear);
|
||||
if(gv!==gearV.textContent){
|
||||
gearV.textContent=gv;
|
||||
gearBox.style.animation='none';gearBox.offsetHeight;gearBox.style.animation='';
|
||||
gearV.classList.add('flash');setTimeout(()=>gearV.classList.remove('flash'),150);
|
||||
}
|
||||
|
||||
// Throttle / Brake
|
||||
tFill.style.width=(d.throttle*100).toFixed(0)+'%';
|
||||
bFill.style.width=(d.brake*100).toFixed(0)+'%';
|
||||
|
||||
// Fuel
|
||||
const fuelPct=Math.round((d.fuel||1)*100);
|
||||
fFill.style.width=fuelPct+'%';
|
||||
fVal.textContent=fuelPct+'%';
|
||||
|
||||
// Water temp (estimate from engine temp or oil temp raw)
|
||||
// Forza doesn't send exact water temp, using engine_temp if available
|
||||
const rpm=d.rpm||0,mr=d.max_rpm||8000,g=d.gear||0,sp=d.speed_kmh||0;
|
||||
if(Math.abs(rpm-lR)>30||g!==lG){lR=rpm;lG=g;drawRpm(rpm,mr,g)}
|
||||
sV.textContent=Math.round(sp);
|
||||
const gv=g===0?'N':g===11?'R':g===21?'P':String(g);
|
||||
gV.textContent=gv;
|
||||
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 pct=Math.min(et/120,1)*100;
|
||||
wFill.style.width=pct+'%';
|
||||
wVal.textContent=Math.round(et)+'°C';
|
||||
}
|
||||
if(et>0){const ep=Math.min(et/120,1)*100;wF.style.width=ep+'%';wV.textContent=Math.round(et)+'\u00B0C'}
|
||||
}
|
||||
|
||||
drawArc(8000,0,1);
|
||||
apply();window.addEventListener('resize',apply);connect();
|
||||
drawRpm(0,8000,1);apply();window.addEventListener('resize',apply);connect()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -6,71 +6,53 @@
|
||||
<title>TurboSu - 档位</title>
|
||||
<style>
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;
|
||||
font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
||||
font-family:'SF Pro Display',-apple-system,system-ui,sans-serif}
|
||||
#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}
|
||||
#box.center{width:auto;height:auto}
|
||||
#content{width:100%;height:100%;display:flex;align-items:center;justify-content:center;
|
||||
background:linear-gradient(135deg,#0a0a14,#1a1a30);flex-direction:column;gap:min(2.5vh,16px)}
|
||||
.gear-circle{position:relative;width:min(28vmin,120px);height:min(28vmin,120px);
|
||||
border-radius:50%;background:rgba(52,130,255,.08);border:3px solid rgba(52,130,255,.3);
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow:0 0 40px rgba(52,130,255,.1);transition:border-color .2s,border-width .2s}
|
||||
.gear-circle span{font-size:min(13vmin,50px);font-weight:900;color:#fff;transition:.12s ease}
|
||||
.gear-circle.r-active{border-color:rgba(233,70,52,.55);box-shadow:0 0 50px rgba(233,70,52,.2)}
|
||||
.gear-circle.n-active{border-color:rgba(255,255,255,.25)}
|
||||
.gear-circle.p-active{border-color:rgba(52,199,89,.3)}
|
||||
.bar-row{display:flex;gap:min(1.2vw,6px);align-items:flex-end;padding:0 6px;flex-wrap:wrap;justify-content:center}
|
||||
.bar-col{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:min(7vw,32px)}
|
||||
.bar-label{font-size:min(2.2vmin,12px);font-weight:600;color:rgba(255,255,255,.2);transition:color .2s}
|
||||
.bar-fill{width:min(5vw,24px);height:min(5vw,24px);border-radius:5px;background:rgba(255,255,255,.05);transition:background .2s,transform .15s}
|
||||
.bar-col.active .bar-fill{background:var(--accent,#3482FF);transform:scaleY(1.25)}
|
||||
.bar-col.active .bar-label{color:#fff}
|
||||
.bar-col.r .bar-fill{background:rgba(233,70,52,.25)}
|
||||
.bar-col.r.active .bar-fill{background:#E94634}
|
||||
.bar-col.n .bar-fill{background:rgba(255,255,255,.08)}
|
||||
.bar-col.n.active .bar-fill{background:rgba(255,255,255,.2)}
|
||||
.bar-col.p .bar-fill{background:rgba(52,199,89,.15)}
|
||||
.bar-col.p.active .bar-fill{background:rgba(52,199,89,.5)}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:min(3vh,20px)}
|
||||
.circle{width:min(28vmin,120px);height:min(28vmin,120px);border-radius:50%;border:2px solid rgba(255,255,255,.15);display:flex;align-items:center;justify-content:center}
|
||||
.circle span{font-size:min(14vmin,54px);font-weight:200;transition:.12s ease}
|
||||
.circle.r{border-color:rgba(255,80,60,.4)}
|
||||
.circle.p{border-color:rgba(255,255,255,.2)}
|
||||
.bars{display:flex;gap:min(1.5vw,8px);flex-wrap:wrap;justify-content:center;padding:0 8px}
|
||||
.b{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:min(7vw,28px)}
|
||||
.b .dot{width:min(4vw,16px);height:min(4vw,16px);border-radius:3px;background:rgba(255,255,255,.04)}
|
||||
.b .l{font-size:min(2.2vmin,11px);font-weight:500;color:rgba(255,255,255,.15)}
|
||||
.b.on .dot{background:#fff}
|
||||
.b.on .l{color:#fff}
|
||||
.b.r .dot{background:rgba(255,60,40,.15)}.b.r.on .dot{background:#E94634}
|
||||
.b.n .dot{background:rgba(255,255,255,.06)}.b.n.on .dot{background:rgba(255,255,255,.25)}
|
||||
.b.p .dot{background:rgba(255,255,255,.04)}.b.p.on .dot{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="box" class="contain"><div id="content">
|
||||
<div class="gear-circle" id="gear-el"><span id="gear-txt">-</span></div>
|
||||
<div class="bar-row" id="bar-row"></div>
|
||||
<div class="circle" id="gear-el"><span id="gear-txt">-</span></div>
|
||||
<div class="bars" id="bar-row"></div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"1:1",render_mode:"contain"};
|
||||
let GAME_ID='';
|
||||
|
||||
const AC_STYLE_LABELS=['R','N','1','2','3','4','5','6','7','8'];
|
||||
const AC_LABELS=['R','N','1','2','3','4','5','6','7','8'];
|
||||
const FORZA_LABELS=['N','1','2','3','4','5','6','7','8','9','10','R','P'];
|
||||
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];return w/h}return null}
|
||||
|
||||
function buildBars(){
|
||||
const isForza=GAME_ID&&GAME_ID.includes('forza');
|
||||
const labels=isForza?FORZA_LABELS:AC_STYLE_LABELS;
|
||||
const labels=isForza?FORZA_LABELS:AC_LABELS;
|
||||
barRow.innerHTML=labels.map((l,i)=>{
|
||||
let cls='bar-col';
|
||||
if(l==='R')cls+=' r';
|
||||
if(l==='N')cls+=' n';
|
||||
if(l==='P')cls+=' p';
|
||||
return `<div class="${cls} g-${i}"><div class="bar-fill"></div><div class="bar-label">${l}</div></div>`;
|
||||
let c='b';if(l==='R')c+=' r';if(l==='N')c+=' n';if(l==='P')c+=' p';
|
||||
return `<div class="${c}"><div class="dot"></div><div class="l">${l}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Forza: 0=N, 1-10=1-10, 11=R, 21=P
|
||||
// AC/F1: -1=R, 0=N, 1-8=1-8
|
||||
// iRacing: 0=N, 1=R, 2=1, 3=2...
|
||||
function gearInfo(g){
|
||||
const isForza=GAME_ID&&GAME_ID.includes('forza');
|
||||
const isIracing=GAME_ID&&GAME_ID.includes('iracing');
|
||||
|
||||
const isForza=GAME_ID&&GAME_ID.includes('forza'),isIracing=GAME_ID&&GAME_ID.includes('iracing');
|
||||
if(isForza){
|
||||
if(g===0)return{label:'N',cls:'n',idx:0};
|
||||
if(g===11)return{label:'R',cls:'r',idx:11};
|
||||
@@ -84,48 +66,40 @@ function gearInfo(g){
|
||||
if(g>=2&&g<=9)return{label:String(g-1),cls:'',idx:g};
|
||||
return{label:String(g),cls:'',idx:-1};
|
||||
}
|
||||
// default AC/F1 style
|
||||
if(g===-1)return{label:'R',cls:'r',idx:0};
|
||||
if(g===0)return{label:'N',cls:'n',idx:1};
|
||||
if(g>=1&&g<=8)return{label:String(g),cls:'',idx:g+1};
|
||||
return{label:String(g),cls:'',idx:-1};
|
||||
}
|
||||
|
||||
let lastGear=-999;
|
||||
function updateGear(g){
|
||||
if(g===lastGear)return;lastGear=g;
|
||||
const info=gearInfo(g);
|
||||
gearTxt.textContent=info.label;
|
||||
gearEl.className='gear-circle active';
|
||||
if(info.cls==='r')gearEl.classList.add('r-active');
|
||||
else if(info.cls==='n')gearEl.classList.add('n-active');
|
||||
else if(info.cls==='p')gearEl.classList.add('p-active');
|
||||
barRow.querySelectorAll('.bar-col').forEach((el,i)=>{el.classList.toggle('active',i===info.idx)});
|
||||
let lastG=-999;
|
||||
function update(g){
|
||||
if(g===lastG)return;lastG=g;
|
||||
const i=gearInfo(g);
|
||||
gearTxt.textContent=i.label;
|
||||
gearEl.className='circle';if(i.cls==='r')gearEl.className='circle r';else if(i.cls==='p')gearEl.className='circle p';
|
||||
barRow.querySelectorAll('.b').forEach((el,j)=>{el.classList.toggle('on',j===i.idx)});
|
||||
}
|
||||
|
||||
function apply(){
|
||||
const ratio=parseRatio(M.aspect_ratio||'1:1'),mode=M.render_mode||'contain';
|
||||
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
|
||||
if(!ratio){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;
|
||||
switch(mode){
|
||||
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
|
||||
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
|
||||
case'center':cw=Math.min(vw,800);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,800);cw=ch*ratio};box.className='center';break;
|
||||
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
|
||||
}
|
||||
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
|
||||
if(mode==='cover'){cw=vw;ch=vw/ratio;box.className='cover'}
|
||||
else{box.className='contain';cw=vr>ratio?vh*ratio:vw;ch=vr>ratio?vh:vw/ratio}
|
||||
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||
}
|
||||
|
||||
function connect(){
|
||||
ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}};
|
||||
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||'';buildBars()}
|
||||
if(m.type==='telemetry')updateGear(m.data.gear||0)
|
||||
}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
if(m.type==='telemetry')update(m.data.gear||0)
|
||||
}catch(e){}}
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)}
|
||||
}
|
||||
const box=document.getElementById('box'),gearTxt=document.getElementById('gear-txt'),
|
||||
gearEl=document.getElementById('gear-el'),barRow=document.getElementById('bar-row');
|
||||
|
||||
const box=document.getElementById('box'),gearTxt=document.getElementById('gear-txt'),gearEl=document.getElementById('gear-el'),barRow=document.getElementById('bar-row');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt;
|
||||
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
<title>TurboSu - 圈速</title>
|
||||
<style>
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif;color:#fff}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
||||
font-family:'SF Pro Display',-apple-system,system-ui,sans-serif}
|
||||
#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}
|
||||
#box.center{width:auto;height:auto}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a14,#1a1a30);gap:min(3vw,16px)}
|
||||
.lap-num{font-size:min(3vw,16px);color:rgba(255,255,255,.3);letter-spacing:.15em;margin-bottom:-8px}
|
||||
.current{font-size:min(12vw,72px);font-weight:900;color:#3482FF;font-variant-numeric:tabular-nums}
|
||||
.below{display:flex;gap:min(8vw,48px)}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:min(3vh,20px)}
|
||||
.lap-num{font-size:min(3vw,14px);color:rgba(255,255,255,.2);letter-spacing:.2em;text-transform:uppercase}
|
||||
.current{font-size:min(14vw,90px);font-weight:200;font-variant-numeric:tabular-nums;letter-spacing:-.02em}
|
||||
.below{display:flex;gap:min(10vw,60px)}
|
||||
.col{text-align:center}
|
||||
.col .label{font-size:min(2vw,12px);color:rgba(255,255,255,.35);letter-spacing:.08em;margin-bottom:2px}
|
||||
.col .val{font-size:min(5vw,28px);font-weight:700;font-variant-numeric:tabular-nums}
|
||||
.col:last-child .val{color:#34C759}
|
||||
.col .label{font-size:min(2vw,11px);color:rgba(255,255,255,.2);letter-spacing:.1em;text-transform:uppercase;margin-bottom:4px}
|
||||
.col .val{font-size:min(5vw,26px);font-weight:300;font-variant-numeric:tabular-nums;color:rgba(255,255,255,.5)}
|
||||
.col:last-child .val{color:rgba(255,255,255,.8)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -28,41 +28,37 @@ html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'SF
|
||||
<div class="lap-num">LAP <span id="lap-n">0</span></div>
|
||||
<div class="current" id="cur">00:00.000</div>
|
||||
<div class="below">
|
||||
<div class="col"><div class="label">LAST</div><div class="val" id="last" style="color:rgba(255,255,255,.55)">00:00.000</div></div>
|
||||
<div class="col"><div class="label">LAST</div><div class="val" id="last">00:00.000</div></div>
|
||||
<div class="col"><div class="label">BEST</div><div class="val" id="best">00:00.000</div></div>
|
||||
</div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const M={aspect_ratio:"4:3",render_mode:"contain"};
|
||||
(function(){
|
||||
const box=document.getElementById('box'),curEl=document.getElementById('cur'),lastEl=document.getElementById('last'),bestEl=document.getElementById('best'),lapN=document.getElementById('lap-n');
|
||||
const box=document.getElementById('box');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt;
|
||||
|
||||
function fmt(t){if(!t||t<=0)return'00:00.000';const m=Math.floor(t/60),s=(t%60).toFixed(3);return String(m).padStart(2,'0')+':'+(m>0?String(s).padStart(6,'0'):s)}
|
||||
function fmt(t){if(!t||t<=0)return'00:00.000';const m=Math.floor(t/60),s=(t%60).toFixed(3);return String(m).padStart(2,'0')+':'+s.padStart(6,'0')}
|
||||
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];return w/h}return null}
|
||||
|
||||
function apply(){
|
||||
const ratio=parseRatio(M.aspect_ratio||'4:3'),mode=M.render_mode||'contain';
|
||||
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
|
||||
if(!ratio){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;
|
||||
switch(mode){
|
||||
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
|
||||
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
|
||||
case'center':cw=Math.min(vw,1200);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,900);cw=ch*ratio};box.className='center';break;
|
||||
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
|
||||
}
|
||||
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
|
||||
if(mode==='cover'){cw=vw;ch=vw/ratio;box.className='cover'}
|
||||
else{box.className='contain';cw=vr>ratio?vh*ratio:vw;ch=vr>ratio?vh:vw/ratio}
|
||||
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||
}
|
||||
|
||||
function connect(){
|
||||
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'){const d=m.data;curEl.textContent=fmt(d.lap_time);lastEl.textContent=fmt(d.last_lap);bestEl.textContent=fmt(d.best_lap);lapN.textContent=d.lap_number||0}}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
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'){const d=m.data;document.getElementById('cur').textContent=fmt(d.lap_time);document.getElementById('last').textContent=fmt(d.last_lap);document.getElementById('best').textContent=fmt(d.best_lap);document.getElementById('lap-n').textContent=d.lap_number||0}}catch(e){}}
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)}
|
||||
}
|
||||
|
||||
apply();window.addEventListener('resize',apply);connect();
|
||||
apply();window.addEventListener('resize',apply);connect()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -5,21 +5,19 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
||||
<title>TurboSu - 速度表</title>
|
||||
<style>
|
||||
:root{--ratio:auto;--mode:contain}
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;color:#fff;font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif;-webkit-tap-highlight-color:transparent}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
||||
font-family:'SF Pro Display',-apple-system,system-ui,sans-serif}
|
||||
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
||||
#box{position:relative;overflow:hidden}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a14,#1a1a30)}
|
||||
.speed{font-size:min(18vw,160px);font-weight:900;line-height:1;color:#fff;text-shadow:0 0 50px rgba(52,130,255,.5);transition:font-size .1s ease}
|
||||
.unit{font-size:min(3vw,28px);color:rgba(255,255,255,.55);margin-top:8px;letter-spacing:.3em}
|
||||
.bar{width:min(80%,400px);height:6px;background:rgba(255,255,255,.1);border-radius:3px;overflow:hidden;margin-top:24px}
|
||||
.bar-fill{height:100%;background:linear-gradient(90deg,#3482FF,#764ba2);border-radius:3px;transition:width .08s linear}
|
||||
/* aspect-ratio variants */
|
||||
#box.contain{max-width:100vw;max-height:100vh}
|
||||
#box.cover{min-width:100vw;min-height:100vh}
|
||||
#box.fill{width:100vw;height:100vh}
|
||||
#box.center{width:auto;height:auto}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.speed{font-size:min(20vw,180px);font-weight:200;line-height:1;letter-spacing:-.02em}
|
||||
.unit{font-size:min(3vw,22px);color:rgba(255,255,255,.3);margin-top:8px;letter-spacing:.4em;text-transform:uppercase}
|
||||
.bar{width:min(70%,340px);height:2px;background:rgba(255,255,255,.08);margin-top:32px}
|
||||
.bar-fill{height:100%;background:#fff;transition:width .06s linear}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -31,44 +29,28 @@ html,body{width:100%;height:100%;overflow:hidden;background:#000;color:#fff;font
|
||||
<script>
|
||||
const M={aspect_ratio:"auto",render_mode:"contain"};
|
||||
(function(){
|
||||
const box=document.getElementById('box');
|
||||
const vEl=document.getElementById('v');
|
||||
const bar=document.getElementById('bar');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:';
|
||||
const wsUrl=proto+'//'+location.host+'/ws';
|
||||
const box=document.getElementById('box'),vEl=document.getElementById('v'),bar=document.getElementById('bar');
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt;
|
||||
|
||||
function parseRatio(r){
|
||||
if(!r||r==='auto')return null;
|
||||
const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}
|
||||
return null;
|
||||
}
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];return w/h}return null}
|
||||
|
||||
function apply(){
|
||||
const ratio=parseRatio(M.aspect_ratio||'auto');
|
||||
const mode=M.render_mode||'contain';
|
||||
if(!ratio){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;
|
||||
switch(mode){
|
||||
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
|
||||
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
|
||||
case'center':cw=Math.min(vw,1920);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,1080);cw=ch*ratio};box.className='center';break;
|
||||
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
|
||||
}
|
||||
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
|
||||
const ratio=parseRatio(M.aspect_ratio||'auto'),mode=M.render_mode||'contain';
|
||||
if(!ratio){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(mode==='cover'){cw=vw;ch=vw/ratio;box.className='cover'}
|
||||
else{box.className='contain';cw=vr>ratio?vh*ratio:vw;ch=vr>ratio?vh:vw/ratio}
|
||||
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||
}
|
||||
|
||||
function connect(){
|
||||
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'){const d=m.data;vEl.textContent=(d.speed_kmh||0).toFixed(0);bar.style.width=Math.min((d.speed_kmh||0)/400,1)*100+'%'}}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
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'){const s=m.data.speed_kmh||0;vEl.textContent=s.toFixed(0);bar.style.width=Math.min(s/400,1)*100+'%'}}catch(e){}}
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)}
|
||||
}
|
||||
|
||||
apply();
|
||||
window.addEventListener('resize',apply);
|
||||
connect();
|
||||
apply();window.addEventListener('resize',apply);connect()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -5,35 +5,29 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
||||
<title>TurboSu - 转速表</title>
|
||||
<style>
|
||||
:root{--ratio:16/9;--mode:contain}
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
||||
font-family:'SF Pro Display',-apple-system,system-ui,sans-serif}
|
||||
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
||||
#box{position:relative;overflow:hidden}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#0a0a14,#1a1a30)}
|
||||
svg{width:min(90%,500px)}
|
||||
.rpm-text{font-size:min(8vw,48px);font-weight:900;fill:#fff}
|
||||
.rpm-label{font-size:min(2vw,14px);fill:rgba(255,255,255,.45)}
|
||||
.gauge-bg{fill:none;stroke:rgba(255,255,255,.08);stroke-width:20;stroke-linecap:round}
|
||||
.gauge-fg{fill:none;stroke:url(#g);stroke-width:20;stroke-linecap:round;stroke-dasharray:314;transition:stroke-dashoffset .1s ease}
|
||||
.labels text{font-size:10px;fill:rgba(255,255,255,.25);text-anchor:middle}
|
||||
/* aspect-ratio */
|
||||
#box.contain{max-width:100vw;max-height:100vh}
|
||||
#box.cover{min-width:100vw;min-height:100vh}
|
||||
#box.fill{width:100vw;height:100vh}
|
||||
#box.center{width:auto;height:auto}
|
||||
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
svg{width:min(92%,480px)}
|
||||
.rpm-num{font-size:min(9vw,52px);font-weight:200;fill:#fff;letter-spacing:-.02em}
|
||||
.rpm-label{font-size:min(2vw,12px);fill:rgba(255,255,255,.25);letter-spacing:.15em}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"><div id="box" class="contain"><div id="content">
|
||||
<svg viewBox="0 0 320 200">
|
||||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#3482FF"/><stop offset="40%" stop-color="#3482FF"/><stop offset="70%" stop-color="#FF9F0A"/><stop offset="100%" stop-color="#E94634"/></linearGradient></defs>
|
||||
<path d="M 30,170 A 130,130 0 0,1 290,170" class="gauge-bg"/>
|
||||
<path id="arc" d="M 30,170 A 130,130 0 0,1 290,170" class="gauge-fg" stroke-dashoffset="314"/>
|
||||
<text x="160" y="148" text-anchor="middle" class="rpm-text" id="rpm-v">0</text>
|
||||
<path d="M 20,168 A 140,140 0 0,1 300,168" fill="none" stroke="rgba(255,255,255,.06)" stroke-width="16" stroke-linecap="round"/>
|
||||
<path id="arc" d="M 20,168 A 140,140 0 0,1 300,168" fill="none" stroke="#fff" stroke-width="16" stroke-linecap="round" stroke-dasharray="440" stroke-dashoffset="440" opacity=".9"/>
|
||||
<text x="160" y="148" text-anchor="middle" class="rpm-num" id="rpm-v">0</text>
|
||||
<text x="160" y="172" text-anchor="middle" class="rpm-label">RPM</text>
|
||||
<g class="labels">
|
||||
<text x="28" y="188">0</text><text x="80" y="72">2k</text><text x="160" y="44">4k</text><text x="240" y="72">6k</text><text x="292" y="188">8k</text>
|
||||
<g class="rpm-label" style="font-size:9px">
|
||||
<text x="18" y="188">0</text><text x="88" y="70">2k</text><text x="160" y="44">4k</text><text x="232" y="70">6k</text><text x="302" y="188">8k</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div></div></div>
|
||||
@@ -44,28 +38,24 @@ const M={aspect_ratio:"16:9",render_mode:"contain"};
|
||||
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||
let ws,rt;
|
||||
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
|
||||
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];return w/h}return null}
|
||||
|
||||
function apply(){
|
||||
const ratio=parseRatio(M.aspect_ratio||'16:9'),mode=M.render_mode||'contain';
|
||||
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
|
||||
if(!ratio){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;
|
||||
switch(mode){
|
||||
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
|
||||
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
|
||||
case'center':cw=Math.min(vw,1920);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,1080);cw=ch*ratio};box.className='center';break;
|
||||
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
|
||||
}
|
||||
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
|
||||
if(mode==='cover'){cw=vw;ch=vw/ratio;box.className='cover'}
|
||||
else{box.className='contain';cw=vr>ratio?vh*ratio:vw;ch=vr>ratio?vh:vw/ratio}
|
||||
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||
}
|
||||
|
||||
function connect(){
|
||||
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'){const d=m.data,rpm=d.rpm||0,max=d.max_rpm||8000,pct=Math.min(rpm/max,1);rpmV.textContent=rpm.toFixed(0);arc.setAttribute('stroke-dashoffset',314-314*pct)}}catch(e){}};
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)};
|
||||
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'){const d=m.data,rpm=d.rpm||0,max=d.max_rpm||8000,pct=Math.min(rpm/max,1);rpmV.textContent=rpm.toFixed(0);arc.setAttribute('stroke-dashoffset',440-440*pct)}}catch(e){}}
|
||||
ws.onclose=()=>{rt=setTimeout(connect,2000)}
|
||||
}
|
||||
|
||||
apply();window.addEventListener('resize',apply);connect();
|
||||
apply();window.addEventListener('resize',apply);connect()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user