110 lines
4.8 KiB
HTML
110 lines
4.8 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 - 档位</title>
|
|
<style>
|
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
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}
|
|
#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="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_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];return w/h}return null}
|
|
|
|
function buildBars(){
|
|
const isForza=GAME_ID&&GAME_ID.includes('forza');
|
|
const labels=isForza?FORZA_LABELS:AC_LABELS;
|
|
barRow.innerHTML=labels.map((l,i)=>{
|
|
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('');
|
|
}
|
|
|
|
function gearInfo(g){
|
|
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};
|
|
if(g===21)return{label:'P',cls:'p',idx:12};
|
|
if(g>=1&&g<=10)return{label:String(g),cls:'',idx:g};
|
|
return{label:String(g),cls:'',idx:-1};
|
|
}
|
|
if(isIracing){
|
|
if(g===0)return{label:'N',cls:'n',idx:0};
|
|
if(g===1)return{label:'R',cls:'r',idx:1};
|
|
if(g>=2&&g<=9)return{label:String(g-1),cls:'',idx:g};
|
|
return{label:String(g),cls:'',idx:-1};
|
|
}
|
|
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 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}
|
|
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==='connected'){GAME_ID=m.selected_game_id||'';buildBars()}
|
|
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 proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
|
let ws,rt;
|
|
|
|
buildBars();apply();window.addEventListener('resize',apply);connect();
|
|
</script>
|
|
</body>
|
|
</html>
|