Files
TurboSu/dashboards/forza_composite/index.html
T

258 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 - 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}
#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%)}
/* ---- 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)}
/* ---- 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}
/* ---- 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)}
/* ---- 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)}
/* ---- 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)}
</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>
<!-- 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>
<!-- 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 proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
let ws,rt,lastGear=-999,lastRpm=-1;
// 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)
function degToRad(d){return d*Math.PI/180}
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 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;}
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();
}
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>`;
}
}
// 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}`;
}
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==='connected'){GAME_ID=m.selected_game_id||''}
if(m.type==='telemetry')update(m.data)
}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 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';
}
}
drawArc(8000,0,1);
apply();window.addEventListener('resize',apply);connect();
})();
</script>
</body>
</html>