- Interfaz completamente rediseñada: tema militar/racing oscuro - Fondo con grid animado, tipografia Orbitron - Barras de potencia estilo LED (6 segmentos verde→rojo) - Botones angulares con glow rojo (traccion) y azul (camara) - Flechas CSS puras, crosshair en centro de camara - Esquinas HUD animadas sobre el stream - camera_index.h regenerado desde data/index.html (4.6KB gzipped) - gen_header.py para regenerar el header en el futuro Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
16 KiB
HTML
328 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
<title>COCHE RC</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--red: #e8001e;
|
|
--red-g: rgba(232,0,30,.55);
|
|
--blue: #1a8cff;
|
|
--blue-g: rgba(26,140,255,.45);
|
|
--cyan: #00ccff;
|
|
--green: #00ff88;
|
|
--bg: #04060e;
|
|
--text: #c8d6e8;
|
|
--dim: rgba(160,185,210,.4);
|
|
--grid: rgba(0,170,255,.04);
|
|
--border: rgba(0,170,255,.14);
|
|
}
|
|
*, *::before, *::after {
|
|
margin:0; padding:0; box-sizing:border-box;
|
|
-webkit-tap-highlight-color:transparent; user-select:none;
|
|
}
|
|
html, body {
|
|
width:100%; height:100%; overflow:hidden;
|
|
background:var(--bg); color:var(--text);
|
|
font-family:'Orbitron','Courier New',monospace;
|
|
}
|
|
|
|
/* GRID BG */
|
|
body::before {
|
|
content:''; position:fixed; inset:0; z-index:0; pointer-events:none;
|
|
background:
|
|
repeating-linear-gradient(90deg,transparent 0,transparent 49px,var(--grid) 49px,var(--grid) 50px),
|
|
repeating-linear-gradient(0deg, transparent 0,transparent 49px,var(--grid) 49px,var(--grid) 50px);
|
|
animation:gridMove 25s linear infinite;
|
|
}
|
|
@keyframes gridMove { to { background-position:0 50px; } }
|
|
|
|
/* TOP BAR */
|
|
#bar {
|
|
position:fixed; top:0; left:0; right:0; height:46px;
|
|
background:rgba(4,6,16,.92); border-bottom:1px solid var(--border);
|
|
backdrop-filter:blur(14px);
|
|
display:flex; align-items:center; padding:0 16px; gap:12px; z-index:200;
|
|
}
|
|
.bar-logo {
|
|
font-size:14px; font-weight:900; letter-spacing:5px;
|
|
color:var(--red); text-shadow:0 0 14px var(--red),0 0 30px rgba(232,0,30,.3);
|
|
}
|
|
.bar-div { width:1px; height:22px; background:var(--border); }
|
|
.bar-dot { width:9px; height:9px; border-radius:50%; background:#222; flex-shrink:0; transition:all .4s; }
|
|
.bar-dot.live { background:var(--green); box-shadow:0 0 10px var(--green); animation:pulse 2.4s ease-in-out infinite; }
|
|
.bar-dot.err { background:var(--red); box-shadow:0 0 10px var(--red); animation:blink .9s infinite; }
|
|
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.4} }
|
|
@keyframes blink { 50%{opacity:.15} }
|
|
#bar-status { font-size:9px; letter-spacing:2px; color:var(--dim); }
|
|
.bar-right { margin-left:auto; display:flex; align-items:center; gap:14px; }
|
|
#bar-speed { font-size:10px; letter-spacing:3px; color:var(--cyan); }
|
|
.bar-fw { font-size:9px; letter-spacing:2px; color:rgba(0,204,255,.3); }
|
|
|
|
/* VIDEO */
|
|
#screen {
|
|
position:fixed; top:46px; left:0; right:0; bottom:0;
|
|
background:#000; display:flex; align-items:center; justify-content:center; z-index:1;
|
|
}
|
|
#stream { max-width:100%; max-height:100%; object-fit:contain; display:block; }
|
|
|
|
/* HUD corners */
|
|
.hc {
|
|
position:absolute; width:32px; height:32px;
|
|
border-color:var(--cyan); border-style:solid;
|
|
opacity:.4; z-index:3; pointer-events:none; transition:opacity .5s;
|
|
}
|
|
#hc-tl { top:10px; left:10px; border-width:2px 0 0 2px; }
|
|
#hc-tr { top:10px; right:10px; border-width:2px 2px 0 0; }
|
|
#hc-bl { bottom:195px;left:10px; border-width:0 0 2px 2px; }
|
|
#hc-br { bottom:195px;right:10px; border-width:0 2px 2px 0; }
|
|
#screen.live .hc { opacity:.7; }
|
|
|
|
/* CONTROLS OVERLAY */
|
|
#ctrl {
|
|
position:fixed; bottom:0; left:0; right:0;
|
|
padding:0 12px 14px;
|
|
background:linear-gradient(transparent,rgba(4,6,14,.97) 36%);
|
|
display:flex; align-items:flex-end; justify-content:space-between; gap:8px; z-index:100;
|
|
}
|
|
|
|
/* D-PAD */
|
|
.pad-wrap { display:flex; flex-direction:column; align-items:center; gap:5px; }
|
|
.pad-lbl { font-size:7px; letter-spacing:4px; color:var(--dim); margin-bottom:1px; }
|
|
.dpad { display:grid; grid-template-columns:repeat(3,54px); grid-template-rows:repeat(3,54px); gap:4px; }
|
|
.dpad.sm { grid-template-columns:repeat(3,44px); grid-template-rows:repeat(3,44px); }
|
|
|
|
.btn {
|
|
display:flex; align-items:center; justify-content:center;
|
|
background:rgba(6,10,22,.96); cursor:pointer;
|
|
transition:transform .07s,background .07s,box-shadow .07s;
|
|
touch-action:none; position:relative; overflow:hidden;
|
|
}
|
|
.btn::after { content:''; position:absolute; bottom:0; right:0; border-style:solid; border-width:0 0 9px 9px; }
|
|
|
|
.btn.car { border:1px solid rgba(232,0,30,.3); }
|
|
.btn.car::after { border-color:transparent transparent rgba(232,0,30,.45) transparent; }
|
|
.btn.car.on,.btn.car:active { background:rgba(232,0,30,.18); box-shadow:0 0 18px var(--red-g),inset 0 0 10px rgba(232,0,30,.12); transform:scale(.9); }
|
|
|
|
.btn.cam { border:1px solid rgba(26,140,255,.25); }
|
|
.btn.cam::after { border-color:transparent transparent rgba(26,140,255,.35) transparent; }
|
|
.btn.cam.on,.btn.cam:active { background:rgba(26,140,255,.18); box-shadow:0 0 16px var(--blue-g),inset 0 0 10px rgba(26,140,255,.12); transform:scale(.9); }
|
|
|
|
/* CSS arrows */
|
|
.ar-u { width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent; }
|
|
.ar-d { width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent; }
|
|
.ar-l { width:0;height:0;border-top:10px solid transparent;border-bottom:10px solid transparent; }
|
|
.ar-r { width:0;height:0;border-top:10px solid transparent;border-bottom:10px solid transparent; }
|
|
.btn.car .ar-u { border-bottom:16px solid var(--red); }
|
|
.btn.car .ar-d { border-top:16px solid var(--red); }
|
|
.btn.car .ar-l { border-right:16px solid var(--red); }
|
|
.btn.car .ar-r { border-left:16px solid var(--red); }
|
|
.btn.cam .ar-u { border-bottom:13px solid var(--blue); }
|
|
.btn.cam .ar-d { border-top:13px solid var(--blue); }
|
|
.btn.cam .ar-l { border-right:13px solid var(--blue); }
|
|
.btn.cam .ar-r { border-left:13px solid var(--blue); }
|
|
|
|
/* Crosshair */
|
|
.xhair { position:relative; width:18px; height:18px; color:var(--cyan); }
|
|
.xhair::before { content:''; position:absolute; top:50%; left:2px; right:2px; height:1px; background:currentColor; transform:translateY(-50%); }
|
|
.xhair::after { content:''; position:absolute; left:50%; top:2px; bottom:2px; width:1px; background:currentColor; transform:translateX(-50%); }
|
|
|
|
/* CENTER */
|
|
.center { flex:1; display:flex; flex-direction:column; align-items:center; gap:10px; max-width:175px; }
|
|
|
|
/* Speed segments */
|
|
.spd-lbl { font-size:7px; letter-spacing:4px; color:var(--dim); text-align:center; margin-bottom:7px; }
|
|
.spd-segs { display:flex; gap:3px; width:100%; }
|
|
.seg { flex:1; height:22px; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.07); transition:background .18s,box-shadow .18s; }
|
|
.seg[data-i="1"].lit { background:#00ff88; box-shadow:0 0 8px #00ff88; }
|
|
.seg[data-i="2"].lit { background:#55ff55; box-shadow:0 0 8px #55ff55; }
|
|
.seg[data-i="3"].lit { background:#aaee00; box-shadow:0 0 8px #aaee00; }
|
|
.seg[data-i="4"].lit { background:#ffcc00; box-shadow:0 0 8px #ffcc00; }
|
|
.seg[data-i="5"].lit { background:#ff7700; box-shadow:0 0 8px #ff7700; }
|
|
.seg[data-i="6"].lit { background:#e8001e; box-shadow:0 0 10px #e8001e; }
|
|
|
|
#speed {
|
|
width:100%; -webkit-appearance:none; appearance:none;
|
|
height:4px; background:transparent; cursor:pointer; margin-top:5px; outline:none;
|
|
}
|
|
#speed::-webkit-slider-runnable-track { background:rgba(0,200,255,.12); height:4px; }
|
|
#speed::-webkit-slider-thumb { -webkit-appearance:none; width:14px; height:14px; background:var(--cyan); box-shadow:0 0 8px var(--cyan); cursor:pointer; border-radius:0; }
|
|
|
|
/* Extra buttons */
|
|
.extras { display:flex; gap:8px; }
|
|
.btn-x {
|
|
padding:9px 15px; background:rgba(6,10,22,.96);
|
|
border:1px solid var(--border); color:var(--text);
|
|
font-family:inherit; font-size:17px; cursor:pointer;
|
|
transition:background .1s,box-shadow .1s,transform .07s; position:relative; overflow:hidden;
|
|
}
|
|
.btn-x::after { content:''; position:absolute; bottom:0; right:0; border-style:solid; border-width:0 0 7px 7px; border-color:transparent transparent var(--border) transparent; }
|
|
.btn-x:active { transform:scale(.91); }
|
|
.btn-x.on { background:rgba(232,120,0,.2); border-color:#f48020; box-shadow:0 0 14px rgba(244,128,32,.45); }
|
|
|
|
.hint { font-size:7px; letter-spacing:2px; color:rgba(160,185,210,.18); text-align:center; line-height:2.2; }
|
|
|
|
/* RESPONSIVE */
|
|
@media (max-height:480px) {
|
|
.dpad { grid-template-columns:repeat(3,44px); grid-template-rows:repeat(3,44px); }
|
|
.dpad.sm { grid-template-columns:repeat(3,36px); grid-template-rows:repeat(3,36px); }
|
|
.seg { height:17px; }
|
|
#ctrl { padding:0 10px 10px; }
|
|
#hc-bl,#hc-br { bottom:165px; }
|
|
}
|
|
@media (max-width:380px) {
|
|
.dpad { grid-template-columns:repeat(3,48px); grid-template-rows:repeat(3,48px); }
|
|
.dpad.sm { grid-template-columns:repeat(3,40px); grid-template-rows:repeat(3,40px); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="hc" id="hc-tl"></div>
|
|
<div class="hc" id="hc-tr"></div>
|
|
<div class="hc" id="hc-bl"></div>
|
|
<div class="hc" id="hc-br"></div>
|
|
|
|
<div id="bar">
|
|
<span class="bar-logo">COCHE RC</span>
|
|
<div class="bar-div"></div>
|
|
<div class="bar-dot" id="dot"></div>
|
|
<span id="bar-status">CONECTANDO</span>
|
|
<div class="bar-right">
|
|
<span id="bar-speed">— / 6</span>
|
|
<span class="bar-fw">FW v2</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="screen">
|
|
<img id="stream" alt="">
|
|
</div>
|
|
|
|
<div id="ctrl">
|
|
|
|
<div class="pad-wrap">
|
|
<span class="pad-lbl">TRACCIÓN</span>
|
|
<div class="dpad">
|
|
<div></div><div class="btn car" id="b-F"><div class="ar-u"></div></div><div></div>
|
|
<div class="btn car" id="b-L"><div class="ar-l"></div></div>
|
|
<div></div>
|
|
<div class="btn car" id="b-R"><div class="ar-r"></div></div>
|
|
<div></div><div class="btn car" id="b-B"><div class="ar-d"></div></div><div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="center">
|
|
<div style="width:100%">
|
|
<div class="spd-lbl">POTENCIA</div>
|
|
<div class="spd-segs" id="segs">
|
|
<div class="seg" data-i="1"></div>
|
|
<div class="seg" data-i="2"></div>
|
|
<div class="seg" data-i="3"></div>
|
|
<div class="seg" data-i="4"></div>
|
|
<div class="seg" data-i="5"></div>
|
|
<div class="seg" data-i="6"></div>
|
|
</div>
|
|
<input type="range" id="speed" min="1" max="6" step="1" value="3">
|
|
</div>
|
|
<div class="extras">
|
|
<button class="btn-x" id="b-horn">📯</button>
|
|
<button class="btn-x" id="b-led">💡</button>
|
|
</div>
|
|
<div class="hint">W A S D · FLECHAS · E F R</div>
|
|
</div>
|
|
|
|
<div class="pad-wrap">
|
|
<span class="pad-lbl">CÁMARA</span>
|
|
<div class="dpad sm">
|
|
<div></div><div class="btn cam" id="b-YU"><div class="ar-u"></div></div><div></div>
|
|
<div class="btn cam" id="b-XL"><div class="ar-l"></div></div>
|
|
<div class="btn cam" id="b-C"><div class="xhair"></div></div>
|
|
<div class="btn cam" id="b-XR"><div class="ar-r"></div></div>
|
|
<div></div><div class="btn cam" id="b-YD"><div class="ar-d"></div></div><div></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const base=document.location.origin,host=window.location.hostname;
|
|
const strmSrc=base.startsWith('https')?`https://stream${host}/stream`:`${base.replace(/:\d+$/,'')}:81/stream`;
|
|
const view=document.getElementById('stream'),screen=document.getElementById('screen');
|
|
const dot=document.getElementById('dot'),stLbl=document.getElementById('bar-status');
|
|
|
|
function startStream(){view.src=strmSrc+'?t='+Date.now();}
|
|
view.onload =()=>{dot.className='bar-dot live';stLbl.textContent='EN VIVO';screen.classList.add('live');};
|
|
view.onerror=()=>{dot.className='bar-dot err';stLbl.textContent='RECONECTANDO';screen.classList.remove('live');setTimeout(startStream,2500);};
|
|
startStream();
|
|
|
|
let mvCtrl=null,ledOn=false,hornOn=false;
|
|
function send(cmd){
|
|
let q;
|
|
if(cmd==='K'){ledOn=!ledOn;document.getElementById('b-led').classList.toggle('on',ledOn);fetch(`${base}/control?var=led_intensity&val=${ledOn?255:0}`).catch(()=>{});return;}
|
|
q=`${base}/control?var=car&val=${cmd}`;
|
|
if('FBLRS'.includes(cmd)&&cmd.length===1){if(mvCtrl)mvCtrl.abort();mvCtrl=new AbortController();fetch(q,{signal:mvCtrl.signal}).catch(()=>{});}
|
|
else fetch(q).catch(()=>{});
|
|
}
|
|
|
|
const spEl=document.getElementById('speed'),segs=document.querySelectorAll('.seg'),spLbl=document.getElementById('bar-speed');
|
|
function applySpeed(){const v=+spEl.value;spLbl.textContent=`PWR ${v} / 6`;segs.forEach(s=>s.classList.toggle('lit',+s.dataset.i<=v));send('V'+Math.round(150+(v-1)*21));}
|
|
spEl.addEventListener('input',applySpeed);applySpeed();
|
|
|
|
let pX=90,pY=90;const ST=10;
|
|
|
|
[['b-F','F'],['b-B','B'],['b-L','L'],['b-R','R']].forEach(([id,cmd])=>{
|
|
const el=document.getElementById(id);
|
|
const dn=()=>{el.classList.add('on');send(cmd);};
|
|
const up=()=>{el.classList.remove('on');send('S');};
|
|
el.addEventListener('mousedown',dn);el.addEventListener('mouseup',up);el.addEventListener('mouseleave',up);
|
|
el.addEventListener('touchstart',e=>{e.preventDefault();dn();},{passive:false});
|
|
el.addEventListener('touchend',e=>{e.preventDefault();up();},{passive:false});
|
|
el.addEventListener('touchcancel',e=>{e.preventDefault();up();},{passive:false});
|
|
});
|
|
|
|
const camA={'b-YU':()=>{pY=Math.max(0,pY-ST);send('Y'+pY);},'b-YD':()=>{pY=Math.min(180,pY+ST);send('Y'+pY);},'b-XL':()=>{pX=Math.min(180,pX+ST);send('X'+pX);},'b-XR':()=>{pX=Math.max(0,pX-ST);send('X'+pX);},'b-C':()=>{pX=90;pY=90;send('X90');send('Y90');}};
|
|
Object.entries(camA).forEach(([id,act])=>{
|
|
const el=document.getElementById(id);
|
|
const dn=()=>{el.classList.add('on');act();};const up=()=>el.classList.remove('on');
|
|
el.addEventListener('mousedown',dn);el.addEventListener('mouseup',up);el.addEventListener('mouseleave',up);
|
|
el.addEventListener('touchstart',e=>{e.preventDefault();dn();},{passive:false});
|
|
el.addEventListener('touchend',e=>{e.preventDefault();up();},{passive:false});
|
|
el.addEventListener('touchcancel',e=>{e.preventDefault();up();},{passive:false});
|
|
});
|
|
|
|
const bH=document.getElementById('b-horn');
|
|
const sH=()=>{if(!hornOn){hornOn=true;bH.classList.add('on');send('H1');}};
|
|
const eH=()=>{if(hornOn){hornOn=false;bH.classList.remove('on');send('H0');}};
|
|
bH.addEventListener('mousedown',sH);bH.addEventListener('mouseup',eH);bH.addEventListener('mouseleave',eH);
|
|
bH.addEventListener('touchstart',e=>{e.preventDefault();sH();},{passive:false});
|
|
bH.addEventListener('touchend',e=>{e.preventDefault();eH();},{passive:false});
|
|
bH.addEventListener('touchcancel',e=>{e.preventDefault();eH();},{passive:false});
|
|
document.getElementById('b-led').addEventListener('click',()=>send('K'));
|
|
|
|
const held=new Set();
|
|
document.addEventListener('keydown',e=>{
|
|
if(e.repeat||held.has(e.key))return;held.add(e.key);
|
|
switch(e.key){
|
|
case'w':case'W':send('F');break;case's':case'S':send('B');break;
|
|
case'a':case'A':send('L');break;case'd':case'D':send('R');break;
|
|
case'ArrowUp':pY=Math.max(0,pY-ST);send('Y'+pY);break;
|
|
case'ArrowDown':pY=Math.min(180,pY+ST);send('Y'+pY);break;
|
|
case'ArrowLeft':pX=Math.min(180,pX+ST);send('X'+pX);break;
|
|
case'ArrowRight':pX=Math.max(0,pX-ST);send('X'+pX);break;
|
|
case'e':case'E':sH();break;case'f':case'F':send('K');break;
|
|
case'r':case'R':pX=90;pY=90;send('X90');send('Y90');break;
|
|
case'+':case'=':spEl.value=Math.min(6,+spEl.value+1);applySpeed();break;
|
|
case'-':case'_':spEl.value=Math.max(1,+spEl.value-1);applySpeed();break;
|
|
}
|
|
});
|
|
document.addEventListener('keyup',e=>{
|
|
held.delete(e.key);
|
|
if('wWsSaAdD'.includes(e.key)&&e.key.trim())send('S');
|
|
if('eE'.includes(e.key))eH();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|