Files
labmagic/dock.html

371 líneas
22 KiB
HTML
Original Blame Histórico

Este archivo contiene caracteres Unicode ambiguos

Este archivo contiene caracteres Unicode que pueden confundirse con otros caracteres. Si crees que esto es intencional, puedes ignorar esta advertencia. Usa el botón de Escape para revelarlos.

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Labmagic</title>
<link rel="icon" href="/icon.png">
<style>
:root {
--bg: #0d0d0d; --hud: #000; --card: #181818; --border: #2a2a2a; --btn: #2b2b2b; --btn-hover: #3c3c3c;
--trough: #303030; --knob: #ececec; --fg: #f3f3f3; --muted: #9b9b9b; --dim: #555; --rec: #e3262c;
--on: #3ad66c; --busy: #f0a500; --off: #4a4a4a; --sheet: #161616;
}
* { box-sizing: border-box; -webkit-user-select: none; user-select: none; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
font: 13px "Segoe UI", system-ui, sans-serif; overflow-x: hidden; }
button { font: inherit; color: var(--fg); background: var(--btn); border: 1px solid transparent; border-radius: 6px;
padding: 7px 10px; cursor: pointer; }
button:hover { background: var(--btn-hover); }
button:active { background: #555; }
button:disabled { opacity: .45; cursor: default; }
button.on { border-color: var(--rec); background: #2a1416; }
button.big { font-size: 16px; font-weight: 600; padding: 12px 10px; letter-spacing: .5px; }
button.sq { padding: 7px 0; width: 30px; }
/* HUD */
.hud { background: var(--hud); padding: 7px 10px 6px; }
.top { display: flex; align-items: center; gap: 8px; }
.led { width: 11px; height: 11px; border-radius: 50%; background: var(--off); flex: none; }
.led.on { background: var(--on); box-shadow: 0 0 6px var(--on); }
.led.busy { background: var(--busy); }
.name { font-weight: 600; font-size: 14px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.name:hover { text-decoration: underline; }
.tc { margin-left: auto; font: 600 22px "Consolas", "Segoe UI", monospace; letter-spacing: 1px; white-space: nowrap; }
.tc.rec { color: var(--rec); }
.dot { color: transparent; font-size: 12px; width: 10px; }
.dot.rec { color: var(--rec); animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { color: transparent; } }
.status { color: var(--muted); font-size: 11px; margin-top: 1px; display: flex; gap: 8px; }
.status .pw { margin-left: auto; }
.cells { display: grid; grid-template-columns: repeat(6, 1fr); gap: 3px; margin-top: 6px; }
@media (max-width: 330px) { .cells { grid-template-columns: repeat(3, 1fr); } }
.cell { background: #0a0a0a; border: 1px solid #1c1c1c; border-radius: 6px; padding: 4px 2px 5px; text-align: center; }
.cell .l { color: var(--muted); font-size: 9px; letter-spacing: .8px; }
.cell .v { font: 600 15px "Segoe UI", sans-serif; margin-top: 1px; white-space: nowrap; overflow: hidden; }
.cell.click { cursor: pointer; }
.cell.click .v { border-bottom: 1px solid #3a3a3a; display: inline-block; padding: 0 2px; }
.cell.click:hover { background: #151515; }
.cell.click:hover .v { border-bottom-color: var(--fg); }
.banner { display: none; padding: 6px 10px; font-size: 12px; background: var(--rec); }
.banner.warn { background: #8a5a00; }
.banner.ok { background: #1f4d2b; }
.banner.show { display: block; }
main { padding: 8px 10px 10px; display: flex; flex-direction: column; gap: 8px; }
.row { display: flex; gap: 6px; align-items: stretch; }
.row > .grow { flex: 1; }
.tog { font-size: 11.5px; padding: 6px 8px; flex: 1; white-space: nowrap; }
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: -1px; margin-right: 4px;
border: 1px solid #666; }
.srow { display: grid; grid-template-columns: 44px 30px 1fr 30px; gap: 5px; align-items: center; }
.srow .lab { font-size: 12px; color: var(--muted); }
.srow .lab b { display: block; color: var(--fg); font-weight: 600; font-size: 12px; white-space: nowrap; }
input[type=range] { -webkit-appearance: none; width: 100%; height: 28px; background: transparent; margin: 0; cursor: pointer; }
input[type=range]:disabled { opacity: .4; cursor: default; }
input[type=range]::-webkit-slider-runnable-track { height: 6px; background: var(--trough); border-radius: 3px; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%;
background: var(--knob); margin-top: -6px; box-shadow: 0 1px 3px #000; }
.rec-row { display: flex; align-items: center; gap: 12px; background: var(--card); border-radius: 8px; padding: 8px 10px; }
.recbtn { width: 46px; height: 46px; border-radius: 50%; background: #1a1a1a; border: 3px solid #444; position: relative;
flex: none; padding: 0; }
.recbtn::after { content: ""; position: absolute; inset: 7px; border-radius: 50%; background: var(--rec); transition: inset .15s, border-radius .15s; }
.recbtn:hover { border-color: #777; }
.recbtn.rec::after { inset: 12px; border-radius: 4px; }
.recinfo .s { font-weight: 700; letter-spacing: 1px; }
.recinfo .s.rec { color: var(--rec); }
.recinfo .i { color: var(--muted); font-size: 11px; margin-top: 2px; }
.foot { display: flex; gap: 8px; color: var(--dim); font-size: 10.5px; align-items: center; }
.foot a { color: var(--muted); cursor: pointer; margin-left: auto; white-space: nowrap; }
.foot a:hover { color: var(--fg); }
/* Selector estilo cámara (hoja inferior) */
.veil { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: none; }
.veil.show { display: block; }
.sheet { position: fixed; left: 0; right: 0; bottom: 0; background: var(--sheet); border-top: 1px solid var(--border);
border-radius: 12px 12px 0 0; padding: 10px 12px 12px; transform: translateY(100%); transition: transform .18s ease-out;
max-height: 85%; overflow-y: auto; }
.sheet.show { transform: none; }
.sh-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.sh-head .t { color: var(--muted); font-size: 11px; letter-spacing: 1px; }
.sh-head .cur { font: 600 20px "Segoe UI", sans-serif; }
.sh-head .x { margin-left: auto; color: var(--muted); cursor: pointer; padding: 0 4px; font-size: 16px; }
.sh-head .x:hover { color: var(--fg); }
.chips { display: grid; grid-template-columns: repeat(auto-fill, minmax(54px, 1fr)); gap: 4px; margin-bottom: 6px; }
.chip { background: #222; border: 1px solid transparent; border-radius: 6px; padding: 7px 2px; text-align: center;
font-weight: 600; cursor: pointer; font-size: 13px; white-space: nowrap; }
.chip:hover { background: #333; }
.chip.dim { color: var(--muted); font-weight: 400; background: #1c1c1c; }
.chip.sel { background: var(--fg); color: #000; }
.chip.wide { grid-column: span 2; }
.sub { color: var(--muted); font-size: 10.5px; letter-spacing: .8px; margin: 6px 0 3px; }
.note { color: var(--dim); font-size: 10.5px; margin-top: 4px; }
.kv { display: grid; grid-template-columns: 36px 1fr 36px; gap: 6px; align-items: center; font-size: 10.5px; color: var(--muted); }
.ticks { display: flex; justify-content: space-between; padding: 0 42px; color: var(--dim); font-size: 9px; }
</style>
</head>
<body>
<header class="hud">
<div class="top">
<span class="led" id="led"></span>
<span class="name" id="name" title="Pulsa para conectar o desconectar">Sin cámara</span>
<span class="tc" id="tc">00:00:00:00</span><span class="dot" id="dot">●</span>
</div>
<div class="status"><span id="status">Buscando Labmagic…</span><span class="pw" id="power"></span></div>
<div class="cells">
<div class="cell"><div class="l">FPS</div><div class="v" id="c_fps">—</div></div>
<div class="cell click" data-pick="shutter"><div class="l">SHUTTER</div><div class="v" id="c_sh">—</div></div>
<div class="cell click" data-pick="iris"><div class="l">IRIS</div><div class="v" id="c_iris">—</div></div>
<div class="cell click" data-pick="iso"><div class="l">ISO</div><div class="v" id="c_iso">—</div></div>
<div class="cell click" data-pick="wb"><div class="l">WB</div><div class="v" id="c_wb">—</div></div>
<div class="cell click" data-pick="wb"><div class="l">TINT</div><div class="v" id="c_tint">—</div></div>
</div>
</header>
<div class="banner" id="banner"></div>
<main>
<div class="row">
<button class="big grow" id="af" title="Autofoco (también con la barra espaciadora)">AUTOFOCO&nbsp; (AF)</button>
<button class="tog" id="peak" style="flex:0 0 auto" title="Focus assist con líneas de color en la salida elegida en Labmagic"><span class="swatch" id="peaksw"></span>PEAK</button>
</div>
<div class="row">
<button class="tog" id="zebra" title="Zebra en la salida elegida">ZEBRA</button>
<button class="tog" id="false" title="Falso color en la salida elegida">FALSO COLOR</button>
<button class="tog" id="afauto" title="Vuelve a enfocar cada pocos segundos (intervalo en Labmagic → Ajustes)">AF AUTO</button>
</div>
<div class="srow">
<div class="lab">Foco<b id="focus_txt">—</b></div>
<button class="sq" id="f_minus" title="Más cerca (Mayús: paso grande)">◀</button>
<input type="range" id="focus" min="0" max="1000" value="500" disabled>
<button class="sq" id="f_plus" title="Más lejos (Mayús: paso grande)">▶</button>
</div>
<div class="srow">
<div class="lab">Iris<b id="iris_txt">—</b></div>
<button class="sq" id="i_minus" title="Abrir">◀</button>
<input type="range" id="iris" min="0" max="1000" value="0" disabled>
<button class="sq" id="i_plus" title="Cerrar">▶</button>
</div>
<div class="rec-row">
<button class="recbtn" id="rec" title="Grabar / parar"></button>
<div class="recinfo"><div class="s" id="recstate">GRABAR</div><div class="i" id="recinfo">—</div></div>
</div>
<div class="foot"><span id="lens"></span><a id="open" title="Trae la ventana de Labmagic al frente">abrir Labmagic ⇱</a></div>
</main>
<div class="veil" id="veil"></div>
<div class="sheet" id="sheet"></div>
<script>
(() => {
const $ = id => document.getElementById(id);
const ISO_FULL = [100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600];
const ISO_ALL = [100,125,160,200,250,320,400,500,640,800,1000,1250,1600,2000,2500,3200,4000,5000,6400,8000,10000,12800,16000,20000,25600];
const ANG_MAIN = [45, 90, 172.8, 180, 270, 360];
const ANG_ALL = [11.2,15,22.5,30,37.5,45,60,72,90,108,120,144,150,172.8,180,216,270,360];
const SPEEDS = [24,25,30,48,50,60,100,120,125,200,250,500,1000,2000];
const FSTOPS = [1.4,1.8,2.0,2.8,3.5,4.0,4.5,5.6,6.3,8.0,11,16,22];
const WB_CHIPS = [["☀ Día",5600,10],["💡 Tungst.",3200,0],["▭ Fluor.",4000,15],["☁ Nublado",6500,10],["⌂ Sombra",7500,10]];
const PEAK_COL = ["#e3262c","#3ad66c","#3b82f6","#f3f3f3","#000"];
const PEAK_NAME = ["Rojo","Verde","Azul","Blanco","Negro"];
let S = {}; // último estado recibido
let dragging = null; // deslizador que se está arrastrando ("focus" | "iris" | "wb" | "tint")
let bannerT = null;
let alive = false;
const cmd = (c, extra = {}) =>
fetch("/cmd", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cmd: c, ...extra }) })
.catch(() => {});
// Envío con límite de ritmo para los deslizadores
const pending = {};
let flushT = null;
function throttled(key, fn) {
pending[key] = fn;
if (!flushT) flushT = setTimeout(() => { flushT = null; const p = { ...pending }; for (const k in pending) delete pending[k]; Object.values(p).forEach(f => f()); }, 120);
}
function banner(text, level = "error", secs = 6) {
const b = $("banner");
b.textContent = text; b.className = "banner show " + level;
clearTimeout(bannerT);
bannerT = setTimeout(() => b.className = "banner", secs * 1000);
}
function render(s) {
S = s;
const on = !!s.connected;
$("led").className = "led " + (on ? "on" : (s.busy ? "busy" : ""));
$("name").textContent = s.name || "Sin cámara";
$("status").textContent = s.status || "";
$("power").textContent = s.power || "";
$("tc").textContent = s.tc || "00:00:00:00";
$("tc").className = "tc" + (s.rec ? " rec" : "");
$("dot").className = "dot" + (s.rec ? " rec" : "");
$("c_fps").textContent = s.fps || "—";
$("c_sh").textContent = s.shutter || "—";
$("c_iris").textContent = s.iris || "—";
$("c_iso").textContent = s.iso || "—";
$("c_wb").textContent = s.wb ? s.wb + "K" : "—";
$("c_tint").textContent = (s.tint === 0 || s.tint) ? (s.tint > 0 ? "+" + s.tint : String(s.tint)) : "—";
$("peak").classList.toggle("on", !!s.peak);
$("peaksw").style.background = PEAK_COL[s.peak_colour || 0];
$("zebra").classList.toggle("on", !!s.zebra);
$("false").classList.toggle("on", !!s.false);
$("afauto").classList.toggle("on", !!s.af_auto);
for (const id of ["af","peak","zebra","false","afauto","f_minus","f_plus","i_minus","i_plus","rec"]) $(id).disabled = !on;
$("focus").disabled = !on; $("iris").disabled = !on;
if (dragging !== "focus") { if (s.focus !== null && s.focus !== undefined) $("focus").value = Math.round(s.focus * 1000); }
if (dragging !== "iris") { if (s.aperture !== null && s.aperture !== undefined) $("iris").value = Math.round(s.aperture * 1000); }
$("focus_txt").textContent = s.focus_text || "—";
$("iris_txt").textContent = s.iris || "—";
$("rec").classList.toggle("rec", !!s.rec);
$("recstate").textContent = s.rec ? "GRABANDO" : "GRABAR";
$("recstate").className = "s" + (s.rec ? " rec" : "");
$("recinfo").textContent = s.rec_info || (on ? "" : "—");
$("lens").textContent = s.lens || "";
if (s.notice && s.notice.t !== (render.lastNotice || 0)) { render.lastNotice = s.notice.t; banner(s.notice.text, s.notice.level, s.notice.secs || 6); }
if (sheetKind) refreshSheet();
}
// --- conexión con Labmagic (SSE con reintento)
function listen() {
const es = new EventSource("/events");
es.onopen = () => { alive = true; };
es.onmessage = e => { try { render(JSON.parse(e.data)); } catch (_) {} };
es.onerror = () => {
es.close(); alive = false;
$("led").className = "led"; $("status").textContent = "Labmagic no está abierta: ábrela y este panel se conecta solo";
setTimeout(listen, 2000);
};
}
listen();
// --- botones
$("name").onclick = () => cmd("connect");
$("af").onclick = () => { cmd("af"); const b = $("af"); b.textContent = "AF ● enviado"; setTimeout(() => b.innerHTML = "AUTOFOCO&nbsp; (AF)", 900); };
$("peak").onclick = () => cmd("peak", { on: !S.peak });
$("peak").oncontextmenu = e => { e.preventDefault(); openSheet("peak"); };
$("zebra").onclick = () => cmd("zebra", { on: !S.zebra });
$("false").onclick = () => cmd("false", { on: !S.false });
$("afauto").onclick = () => cmd("af_auto", { on: !S.af_auto });
$("rec").onclick = () => cmd("record");
$("open").onclick = () => cmd("show");
const step = e => (e.shiftKey ? 0.05 : 0.01);
$("f_minus").onclick = e => cmd("focus_delta", { d: -step(e) });
$("f_plus").onclick = e => cmd("focus_delta", { d: step(e) });
$("i_minus").onclick = e => cmd("iris_delta", { d: -0.05 });
$("i_plus").onclick = e => cmd("iris_delta", { d: 0.05 });
for (const [id, key, c] of [["focus", "focus", "focus"], ["iris", "iris", "iris"]]) {
const el = $(id);
el.oninput = () => { dragging = key; const v = el.value / 1000; if (key === "focus") $("focus_txt").textContent = v.toFixed(2); throttled(c, () => cmd(c, { v })); };
el.onchange = () => { const v = el.value / 1000; cmd(c, { v }); setTimeout(() => dragging = null, 300); };
}
// Teclado: espacio = AF, flechas = foco (cuando el panel tiene el foco)
document.addEventListener("keydown", e => {
if (!S.connected) return;
if (e.code === "Space") { e.preventDefault(); $("af").click(); }
else if (e.code === "ArrowLeft") { e.preventDefault(); cmd("focus_delta", { d: -step(e) }); }
else if (e.code === "ArrowRight") { e.preventDefault(); cmd("focus_delta", { d: step(e) }); }
});
// --- selectores (hoja inferior)
let sheetKind = null;
const wbState = { k: 5600, t: 0 };
document.querySelectorAll(".cell.click").forEach(c => c.onclick = () => openSheet(c.dataset.pick));
$("veil").onclick = closeSheet;
function closeSheet() { sheetKind = null; $("sheet").classList.remove("show"); $("veil").classList.remove("show"); }
function openSheet(kind) {
if (!S.connected) { banner("Conecta la cámara para cambiar valores", "warn", 3); return; }
sheetKind = kind;
if (kind === "wb") { wbState.k = S.wb || 5600; wbState.t = S.tint || 0; }
buildSheet(kind);
$("sheet").classList.add("show"); $("veil").classList.add("show");
}
const h = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; };
function chips(parent, items, cur, onpick, dim) {
const g = h("div", "chips");
for (const [label, val] of items) {
const c = h("div", "chip" + (dim && dim.has(val) ? " dim" : "") + (cur !== null && cur !== undefined && Math.abs(cur - val) < 1e-6 ? " sel" : ""), label);
c.onclick = () => onpick(val);
g.appendChild(c);
}
parent.appendChild(g);
return g;
}
function head(parent, title, cur) {
const hd = h("div", "sh-head");
hd.appendChild(h("span", "t", title)); const cv = h("span", "cur", cur); hd.appendChild(cv);
const x = h("span", "x", "✕"); x.onclick = closeSheet; hd.appendChild(x);
parent.appendChild(hd);
return cv;
}
function refreshSheet() { if (sheetKind && sheetKind !== "wb") buildSheet(sheetKind); }
function buildSheet(kind) {
const sh = $("sheet"); sh.innerHTML = "";
if (kind === "iso") {
const cur = parseInt(S.iso) || null;
head(sh, "ISO", S.iso || "—");
const pick = v => { cmd("iso", { v }); closeSheet(); };
chips(sh, ISO_FULL.map(v => [String(v), v]), cur, pick);
chips(sh, ISO_ALL.filter(v => !ISO_FULL.includes(v)).map(v => [String(v), v]), cur, pick, new Set(ISO_ALL));
sh.appendChild(h("div", "note", "Nativos de la Pocket 4K: 400 y 3200 · tercios en gris"));
} else if (kind === "shutter") {
head(sh, "OBTURADOR", S.shutter || "—");
const curA = S.angle, curS = S.speed;
sh.appendChild(h("div", "sub", "ÁNGULO"));
const pa = a => { cmd("angle", { a }); closeSheet(); };
chips(sh, ANG_MAIN.map(a => [a + "°", a]), curA, pa);
chips(sh, ANG_ALL.filter(a => !ANG_MAIN.includes(a)).map(a => [a + "°", a]), curA, pa, new Set(ANG_ALL));
sh.appendChild(h("div", "sub", "VELOCIDAD 1/x"));
chips(sh, SPEEDS.map(s => ["1/" + s, s]), curS, s => { cmd("speed", { s }); closeSheet(); });
} else if (kind === "iris") {
head(sh, "IRIS", S.iris || "—");
chips(sh, FSTOPS.map(f => ["f/" + f, f]), null, f => { cmd("iris_f", { f }); closeSheet(); });
const kv = h("div", "kv"); kv.appendChild(h("span", "", "abierto"));
const r = h("input"); r.type = "range"; r.min = 0; r.max = 1000; r.value = Math.round((S.aperture || 0) * 1000);
r.oninput = () => { dragging = "iris"; throttled("iris", () => cmd("iris", { v: r.value / 1000 })); };
r.onchange = () => { cmd("iris", { v: r.value / 1000 }); setTimeout(() => dragging = null, 300); };
kv.appendChild(r); kv.appendChild(h("span", "", "cerrado")); sh.appendChild(kv);
const g = h("div", "chips"); const a = h("div", "chip", "AUTO IRIS"); a.onclick = () => { cmd("auto_iris"); closeSheet(); }; g.appendChild(a); sh.appendChild(g);
} else if (kind === "wb") {
const cv = head(sh, "BALANCE DE BLANCOS", "");
const refresh = () => cv.textContent = `${wbState.k}K ${wbState.t >= 0 ? "+" : ""}${wbState.t}`;
const send = () => throttled("wb", () => cmd("wb", { k: wbState.k, t: wbState.t }));
refresh();
const kv = h("div", "kv"); kv.appendChild(h("span", "", "2500"));
const ks = h("input"); ks.type = "range"; ks.min = 2500; ks.max = 10000; ks.step = 50; ks.value = wbState.k;
ks.oninput = () => { wbState.k = +ks.value; refresh(); send(); markPreset(); };
kv.appendChild(ks); kv.appendChild(h("span", "", "10000")); sh.appendChild(kv);
const ticks = h("div", "ticks"); for (const k of [3200, 4000, 5600, 6500, 7500]) ticks.appendChild(h("span", "", String(k))); sh.appendChild(ticks);
const g = h("div", "chips"); g.style.gridTemplateColumns = "repeat(3, 1fr)";
const presetChips = [];
for (const [label, k, t] of WB_CHIPS) {
const c = h("div", "chip", label); c.dataset.k = k;
c.onclick = () => { wbState.k = k; wbState.t = t; ks.value = k; ts.value = t; refresh(); send(); markPreset(); };
g.appendChild(c); presetChips.push(c);
}
const awb = h("div", "chip", "AUTO WB"); awb.onclick = () => { cmd("awb"); closeSheet(); }; g.appendChild(awb);
sh.appendChild(g);
const markPreset = () => presetChips.forEach(c => c.classList.toggle("sel", +c.dataset.k === wbState.k));
const tr = h("div", "kv"); tr.style.gridTemplateColumns = "36px 1fr 30px 30px";
tr.appendChild(h("span", "", "Tinte"));
const ts = h("input"); ts.type = "range"; ts.min = -50; ts.max = 50; ts.value = wbState.t;
ts.oninput = () => { wbState.t = +ts.value; refresh(); send(); };
tr.appendChild(ts);
for (const [txt, d] of [["−", -1], ["+", 1]]) { const b = h("button", "sq", txt); b.onclick = () => { wbState.t = Math.max(-50, Math.min(50, wbState.t + d)); ts.value = wbState.t; refresh(); send(); }; tr.appendChild(b); }
sh.appendChild(tr);
markPreset();
} else if (kind === "peak") {
head(sh, "COLOR DEL PEAKING", PEAK_NAME[S.peak_colour || 0]);
const g = h("div", "chips");
PEAK_NAME.forEach((n, i) => { const c = h("div", "chip" + (i === (S.peak_colour || 0) ? " sel" : "")); c.innerHTML = `<span class="swatch" style="background:${PEAK_COL[i]}"></span>${n}`; c.onclick = () => { cmd("peak_colour", { c: i }); closeSheet(); }; g.appendChild(c); });
sh.appendChild(g);
sh.appendChild(h("div", "note", "Clic derecho en PEAK abre este selector"));
}
}
})();
</script>
</body>
</html>