FOSFENO/web/panel/index.html
hacklab 61543e1987 MOTORES: pastillas configurables y destino (salida o capa)
Las pestanas de capa de la barra superior no valian: mezclaban navegar por el
panel con elegir sobre que se trabaja, y sacaban del panel de directo. Se
quitan y todo pasa a MOTORES, con dos decisiones separadas:

  DONDE  -> fila 'Donde se ve': SALIDA o una capa del mapping (+ crea una).
  QUE    -> las pastillas: combinaciones guardadas por el usuario.

Pinchar una pastilla la manda al destino elegido: con SALIDA cambia el motor
global, con una capa cambia SOLO esa capa y el resto sigue igual. Con una capa
elegida sus ajustes salen en MOTORES (la tarjeta de propiedades se mueve, no
se duplica: dos formularios para lo mismo se desincronizan a la primera).

Las pastillas son configurables: motor + preset + ajustes y su nombre, con
'Configurar pastillas'. Persisten en data/slots.json (gitignorado, es de cada
instalacion). De fabrica vienen los cinco motores de siempre, asi que quien
actualice se encuentra lo mismo que tenia. Hydra, Mezcla y projectM nativo
solo pueden ir a la salida, y al intentar mandarlos a una capa se dice.

Comprobado en Chromium por CDP, no con --dump-dom: ese fotografia el DOM al
cargar, antes de que llegue el estado por socket, y daba falsos "esta vacio".
Verificado que disparar sobre una capa deja el motor global intacto, que la
tarjeta de propiedades va y vuelve de su sitio, y que las pastillas nuevas se
guardan en disco.

De paso, dos fallos que aparecieron al probarlo:
- Restaurar la vista guardada corria al principio del fichero y llamaba a
  codigo que usa variables declaradas con let mas abajo: ReferenceError que
  se llevaba por delante el resto del script (socket incluido) y dejaba el
  panel en blanco. Ahora se restaura al final.
- #card-destino no estaba en la lista de 'order' del CSS, asi que caia a 0 y
  se colaba encima de las pastillas pese a ir despues en el HTML.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 16:47:01 +02:00

529 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FOSFENO :: Panel</title>
<link rel="stylesheet" href="/panel/panel.css">
<link rel="stylesheet" href="/lib/codemirror/codemirror.css">
<link rel="stylesheet" href="/lib/codemirror/material-darker.css">
</head>
<body>
<!-- Barra superior fija: titulo + pestanas + banda de avisos.
Van juntas en un mismo bloque sticky para que el aviso quede siempre
pegado debajo del titulo, aunque las pestanas salten de linea. -->
<div class="topbar">
<header>
<h1>FOSFENO</h1>
<!-- Dos zonas del panel: MOTORES (lo de siempre) y MAPPING (aparte,
para que no se mezclen los controles de directo con los de
encaje sobre la superficie fisica). -->
<nav class="tabs" id="tabs">
<button class="tab active" data-view="motores">MOTORES</button>
<button class="tab" data-view="mapping">MAPPING<span
id="tab-map-dot" class="tabdot" hidden></span></button>
</nav>
<span id="conn" class="dot off" title="Conexion"></span>
</header>
<!-- Banda de avisos: aqui aparece cualquier error o aviso -->
<div id="notif" class="notif" hidden>
<span id="notif-msg"></span>
<button id="notif-close" aria-label="Cerrar aviso">&times;</button>
</div>
</div>
<!-- En movil las tarjetas van en una sola columna; en pantalla ancha,
en dos columnas (.column pasa a display:contents en movil). -->
<main>
<div class="column">
<!-- Encendido -->
<section class="card center" id="card-power">
<div class="cardhead">
<span class="label">Visuales</span>
<button class="info" data-help="power">i</button>
</div>
<button id="power" class="power-btn">ON</button>
</section>
<!-- Audio: tarjeta + BPM -->
<section class="card" id="card-audio">
<div class="cardhead">
<span class="label">Audio</span>
<button class="info" data-help="audio">i</button>
</div>
<div class="seg" id="audio-source">
<button id="audio-src-mic" class="segbtn" data-src="mic">Microfono (sala)</button>
<button id="audio-src-monitor" class="segbtn" data-src="monitor">Audio del navegador</button>
</div>
<select id="audio-device"><option>Detectando entradas...</option></select>
<button id="audio-apply" class="cmd accent">Aplicar entrada</button>
<button id="dev-rescan" class="cmd">Buscar dispositivos de nuevo</button>
<div class="row">
<span class="label">BPM detectado</span>
<span id="bpm" class="bpm">--</span>
</div>
<p class="hint">Con <b>Audio del navegador</b> las visuales reaccionan a
lo que suena en el equipo (Spotify, YouTube...) aunque lo oigas por los
cascos. Si conectas un microfono con el equipo ya encendido, pulsa
Buscar dispositivos de nuevo.</p>
</section>
<!-- Sensibilidad -->
<section class="card" id="card-sens">
<div class="cardhead">
<span class="label">Sensibilidad:
<span id="sens-val" class="value">1.0</span></span>
<button class="info" data-help="sensibilidad">i</button>
</div>
<input id="sens" type="range" min="0" max="4" step="0.1" value="1">
</section>
<!-- Estado: visual que se esta proyectando ahora mismo -->
<section class="card" id="card-now">
<span class="label">En pantalla</span>
<div id="now" class="now">-</div>
</section>
<!-- Sistema -->
<section class="card sys" id="card-sys">
<button id="reboot" class="sysbtn">Reiniciar</button>
<button id="shutdown" class="sysbtn danger">Apagar</button>
</section>
<p id="net-foot" class="netfoot">FOSFENO</p>
</div>
<div class="column">
<!-- Pastillas: cada circulo es una combinacion guardada (motor +
preset + ajustes) con su nombre. Se disparan sobre el DESTINO que
este elegido abajo: la salida entera o una capa del mapping. -->
<section class="card" id="card-engines">
<div class="cardhead">
<span class="label">Pastillas</span>
<button class="info" data-help="engines">i</button>
</div>
<div class="engines" id="slots"></div>
<div class="row">
<button id="slots-config" class="cmd">Configurar pastillas</button>
</div>
<!-- Editor de una pastilla (solo en modo configurar) -->
<div class="stack" id="slot-editor" hidden>
<div class="row">
<span class="label">Nombre</span>
<input type="text" id="slot-nombre" class="txt" maxlength="12"
placeholder="TUNEL">
</div>
<span class="label">Que lanza</span>
<select id="slot-tipo">
<option value="butter">Visuales MilkDrop</option>
<option value="shader">Shader GLSL</option>
<option value="video">Clip o imagen de la galeria</option>
<option value="camara">Camara</option>
<option value="hydra">Hydra (solo a la salida)</option>
<option value="mixer">Mezclador VJ (solo a la salida)</option>
<option value="projectm-nativo">projectM nativo (solo a la salida)</option>
</select>
<div class="stack slot-op" id="sop-butter" hidden>
<select id="slot-biblioteca">
<option value="butter">Butterchurn &mdash; ~100 presets</option>
<option value="projectm">projectM &mdash; biblioteca .milk</option>
</select>
<select id="slot-preset"><option value="">-- al azar --</option></select>
<div class="stack" id="sop-milk" hidden>
<select id="slot-milk-cat"><option value="">-- categoria --</option></select>
<select id="slot-milk-vis"><option value="">-- visual --</option></select>
<select id="slot-milk-var"><option value="">-- variante --</option></select>
</div>
</div>
<div class="stack slot-op" id="sop-shader" hidden>
<select id="slot-shader"><option value="">-- elige un shader --</option></select>
</div>
<div class="stack slot-op" id="sop-video" hidden>
<select id="slot-video"><option value="">-- elige un clip --</option></select>
</div>
<div class="row">
<button id="slot-borrar" class="cmd del">Borrar pastilla</button>
<button id="slot-listo" class="cmd accent">Listo</button>
</div>
<p class="hint">Las pastillas se guardan en el equipo: al volver
siguen ahi. En directo, pincha una y salta al destino elegido.</p>
</div>
</section>
<!-- Destino: a donde va lo que dispares. La salida entera, o una capa
concreta del mapping. Aqui tambien se crean capas nuevas. -->
<section class="card" id="card-destino">
<div class="cardhead">
<span class="label">Donde se ve</span>
<button class="info" data-help="destino">i</button>
</div>
<div class="subtabs" id="destinos"></div>
<p class="hint" id="destino-hint"></p>
<!-- Cuando el destino es una capa, aqui se mete su tarjeta de
propiedades (la misma de MAPPING, prestada) -->
<div id="destino-props"></div>
</section>
<!-- Butterchurn -->
<section class="card" id="ctl-butterchurn" hidden>
<div class="cardhead">
<span class="label">Butterchurn</span>
<button class="info" data-help="butterchurn">i</button>
</div>
<div class="row">
<button class="cmd" data-action="prev">&laquo; Anterior</button>
<button class="cmd" data-action="next">Siguiente &raquo;</button>
</div>
<select id="bc-preset"><option>Cargando presets...</option></select>
<label class="check">
<input type="checkbox" id="bc-shuffle"> Cambio automatico de preset
</label>
<div class="seg">
<button id="bc-mode-seconds" class="segbtn">Por segundos</button>
<button id="bc-mode-beats" class="segbtn">Al compas (BPM)</button>
</div>
<div class="row">
<span class="label">Intervalo: <span id="bc-int-val">20</span>
<span id="bc-int-unit">s</span></span>
</div>
<input id="bc-interval" type="range" min="1" max="120" step="1" value="20">
<div class="row">
<span class="label">Transicion: <span id="bc-blend-val">2.7</span> s</span>
</div>
<input id="bc-blend" type="range" min="0" max="10" step="0.1" value="2.7">
</section>
<!-- Editor de codigo (Hydra / Shaders) -->
<section class="card" id="ctl-editor" hidden>
<div class="cardhead">
<span class="label" id="editor-title">Editor de codigo</span>
<button class="info" id="editor-info" data-help="hydra">i</button>
</div>
<select id="lib-select"><option>Libreria...</option></select>
<textarea id="code"></textarea>
<div class="row">
<button id="run-code" class="cmd accent">Ejecutar</button>
<button id="clear-code" class="cmd">Limpiar</button>
</div>
<p class="hint" id="editor-hint"></p>
</section>
<!-- Mezclador VJ.
Se lee de arriba abajo como dos capas: el FONDO (capa de abajo:
camara o visuales Butter) y el CLIP de la galeria (capa de
encima), mas la forma de juntarlos. El resumen de mas abajo
dice en una linea que va a salir por el proyector. -->
<section class="card" id="ctl-mixer" hidden>
<div class="cardhead">
<span class="label">Mezclador VJ</span>
<button class="info" data-help="mixer">i</button>
</div>
<!-- 1. Que se ve -->
<span class="label">1 &middot; Que se ve</span>
<div class="seg" id="mixer-source">
<button class="segbtn" data-src="cam">Solo el fondo</button>
<button class="segbtn" data-src="video">Solo el clip</button>
<button class="segbtn" data-src="mix">Los dos</button>
</div>
<!-- 2. Fondo: capa de abajo -->
<div class="row" id="mix-fondo-row">
<span class="label">2 &middot; Fondo (capa de abajo)</span>
<select id="mix-fondo" class="inline">
<option value="cam">Camara</option>
<option value="butter">Visuales Butter (MilkDrop)</option>
</select>
</div>
<!-- Fondo = camara -->
<div class="stack" id="mix-cam-block">
<label class="check">
<input type="checkbox" id="mix-cam"> Camara activada
</label>
<select id="mix-camera"><option value="0">Camara por defecto</option></select>
</div>
<!-- Fondo = visuales Butter: se elige aqui mismo el preset de
MilkDrop que hace de fondo, sin salir del mezclador. -->
<div class="stack" id="mix-bg-block" hidden>
<select id="mix-bg-preset"><option>Cargando presets...</option></select>
<div class="row">
<button id="mix-bg-prev" class="cmd">&laquo; Anterior</button>
<button id="mix-bg-next" class="cmd">Siguiente &raquo;</button>
</div>
<label class="check">
<input type="checkbox" id="mix-bg-shuffle"> Cambiar el fondo solo
</label>
<p class="hint">Es el mismo preset y el mismo cambio automatico del
motor Butter: lo que elijas aqui queda elegido tambien alli.</p>
</div>
<!-- 3. Clip de la galeria: capa de encima -->
<span class="label" id="mix-clip-label">3 &middot; Clip (capa de encima)</span>
<select id="mix-video"><option value="">-- sin video --</option></select>
<div class="row">
<button id="mix-rescan" class="cmd">Actualizar lista</button>
<button id="mix-upload-btn" class="cmd">Subir video o imagen</button>
</div>
<span id="mix-upload-status" class="hint"></span>
<input type="file" id="mix-upload" hidden
accept="video/*,image/jpeg,image/png,image/gif,image/webp">
<!-- 4. Como se juntan las dos capas -->
<div class="row" id="mix-blend-row">
<span class="label">4 &middot; Como se juntan</span>
<select id="mix-blend" class="inline">
<option value="recorte">Recorte (personaje delante)</option>
<option value="blend">Fundido</option>
<option value="diff">Diferencia</option>
<option value="mult">Multiplicar</option>
<option value="add">Sumar</option>
<option value="layer">Capa</option>
</select>
</div>
<!-- Resumen en una linea de lo que sale por el proyector -->
<p class="hint resumen" id="mix-resumen"></p>
<div id="mixer-sliders"></div>
<label class="check">
<input type="checkbox" id="mix-invert"> Invertir colores
</label>
<label class="check">
<input type="checkbox" id="mix-beat"> Pulso al ritmo
</label>
</section>
<!-- projectM -->
<section class="card" id="ctl-projectm" hidden>
<div class="cardhead">
<span class="label">projectM</span>
<button class="info" data-help="projectm">i</button>
</div>
<select id="pm-category">
<option value="">Todas las categorias</option>
</select>
<select id="pm-preset">
<option value="">-- rotando (elige uno para fijarlo) --</option>
</select>
<select id="pm-variante">
<option value="">-- variantes: rotando (elige una a dedo) --</option>
</select>
<div class="row">
<button class="cmd" data-action="prev">&laquo; Anterior</button>
<button class="cmd" data-action="next">Siguiente &raquo;</button>
</div>
<div class="row">
<button id="pm-lock" class="cmd">Congelar visual</button>
<button id="pm-fix" class="cmd">Fijar este estilo</button>
</div>
<p class="hint" id="pm-now">En pantalla: ...</p>
<div class="cardhead">
<span class="label">Favoritos (para pinchar en directo)</span>
</div>
<div class="row">
<button id="pm-fav-add" class="cmd accent">&#9733; Guardar el de pantalla</button>
</div>
<div id="pm-fav-list"></div>
<label class="check">
<input type="checkbox" id="pm-shuffle"> Orden aleatorio
</label>
<div class="row">
<span class="label">Pantalla</span>
<select id="pm-monitor" class="inline">
<option value="auto">Auto (la del escenario)</option>
</select>
</div>
<div class="row">
<span class="label">Segundos por visual: <span id="pm-dur-val">60</span> s</span>
</div>
<input id="pm-duration" type="range" min="5" max="600" step="5" value="60">
<p class="hint">Congelar deja clavado el visual que se ve (al momento,
sin cortes). Fijar este estilo lo apunta en las listas y projectM
rota solo entre variantes de ese look. Cada cambio de listas o
deslizador relanza projectM (~1 s en negro).</p>
</section>
<!-- ===================== PESTANA MAPPING ===================== -->
<!-- Salida: encendido y previsualizacion -->
<section class="card" id="card-mapping" data-vista="mapping">
<div class="cardhead">
<span class="label">Mapping</span>
<button class="info" data-help="mapping">i</button>
</div>
<div class="row">
<label class="check">
<input type="checkbox" id="map-enabled"> Activar mapping
</label>
<label class="check">
<input type="checkbox" id="map-edit"> Editar en el escenario
</label>
</div>
<span class="label">Previsualizacion (arrastra los puntos)</span>
<canvas id="map-preview"></canvas>
<p class="hint">Arrastra los puntos para encajar cada capa en su sitio.
Toca dentro de una capa para seleccionarla. Se guarda solo.</p>
</section>
<!-- Capas: la lista, al estilo de una mesa de VJ -->
<section class="card" id="card-capas" data-vista="mapping">
<div class="cardhead">
<span class="label">Capas</span>
<button class="info" data-help="capas">i</button>
</div>
<div class="row">
<button id="map-add" class="cmd accent">+ Capa</button>
<button id="map-add-mask" class="cmd">+ Mascara</button>
<button id="map-reset" class="cmd">Reset</button>
</div>
<p class="hint"><b>Capa</b> = una zona que PROYECTA algo (eliges que en
Propiedades). <b>Mascara</b> = un recuadro negro que TAPA lo que haya
debajo; no lleva visual.</p>
<div id="map-list"></div>
<p class="hint" id="map-list-vacio">Sin capas: con el mapping activado
y ninguna capa, sale el motor a pantalla completa. Pulsa
<b>+ Capa</b> para trocear la proyeccion en zonas.</p>
</section>
<!-- Propiedades de la capa seleccionada -->
<section class="card" id="card-propiedades" data-vista="mapping">
<div class="cardhead">
<span class="label" id="prop-titulo">Propiedades</span>
<button class="info" data-help="fuentes">i</button>
</div>
<p class="hint" id="prop-vacio">Selecciona una capa (en la lista o
tocando dentro de ella en la previsualizacion) para elegir que
visual se proyecta en esa zona.</p>
<div class="stack" id="prop-capa" hidden>
<div class="row">
<span class="label">Nombre</span>
<input type="text" id="prop-nombre" class="txt" maxlength="24"
placeholder="Capa">
</div>
<span class="label">Que se proyecta aqui</span>
<select id="prop-fuente">
<option value="motor">El motor elegido en MOTORES</option>
<option value="butter">Visuales MilkDrop (propias de esta capa)</option>
<option value="shader">Shader GLSL</option>
<option value="video">Clip o imagen de la galeria</option>
<option value="camara">Camara</option>
<option value="negro">Negro (tapar)</option>
</select>
<!-- Ajustes propios de cada fuente -->
<div class="stack fuente-op" id="fop-butter" hidden>
<span class="label">Biblioteca de presets</span>
<select id="prop-biblioteca">
<option value="butter">Butterchurn &mdash; ~100 presets, los mas ligeros</option>
<option value="projectm">projectM &mdash; tu biblioteca .milk (9795)</option>
</select>
<!-- Biblioteca de Butterchurn -->
<div class="stack" id="fop-bib-butter">
<select id="prop-preset"><option>Cargando presets...</option></select>
<div class="row">
<button id="prop-preset-prev" class="cmd">&laquo; Anterior</button>
<button id="prop-preset-next" class="cmd">Siguiente &raquo;</button>
</div>
</div>
<!-- Biblioteca de projectM: los .milk se traducen al vuelo -->
<div class="stack" id="fop-bib-milk" hidden>
<select id="prop-milk-cat"><option value="">-- categoria --</option></select>
<select id="prop-milk-vis"><option value="">-- visual --</option></select>
<select id="prop-milk-var"><option value="">-- variante --</option></select>
<button id="prop-milk-azar" class="cmd">Uno al azar</button>
<p class="hint" id="prop-milk-est">Los .milk se traducen a
Butterchurn en el propio navegador, al elegirlos. Alguno puede
salir distinto al original: si uno no convence, prueba otra
variante.</p>
</div>
<label class="check">
<input type="checkbox" id="prop-auto"> Cambiar de preset solo
</label>
<div class="row" id="prop-auto-row" hidden>
<span class="label">Cada <span id="prop-seg-val" class="value">20</span> s</span>
</div>
<input id="prop-seg" type="range" min="3" max="180" step="1" value="20" hidden>
<div class="row">
<span class="label">Transicion:
<span id="prop-tr-val" class="value">2.5</span> s</span>
</div>
<input id="prop-transicion" type="range" min="0" max="8" step="0.1" value="2.5">
<p class="hint">A 0 el preset cambia de golpe; subiendolo, uno se
disuelve en el siguiente sin que se note el corte.</p>
</div>
<div class="stack fuente-op" id="fop-shader" hidden>
<select id="prop-shader"><option value="">-- elige un shader --</option></select>
</div>
<div class="stack fuente-op" id="fop-video" hidden>
<select id="prop-video"><option value="">-- elige un clip --</option></select>
</div>
<div class="stack fuente-op" id="fop-camara" hidden>
<select id="prop-camara"><option value="0">Camara por defecto</option></select>
</div>
<div class="row">
<span class="label">Opacidad: <span id="prop-op-val" class="value">100</span>%</span>
</div>
<input id="prop-opacidad" type="range" min="0" max="1" step="0.02" value="1">
<div class="row">
<span class="label">Mezcla con lo de debajo</span>
<select id="prop-mezcla" class="inline">
<option value="normal">Normal (tapa)</option>
<option value="sumar">Sumar (apila luz)</option>
</select>
</div>
<div class="row">
<span class="label">Malla (para curvar)</span>
<span class="grupo">
<button id="map-sub-dec" class="cmd">&minus;</button>
<span id="map-sub-val" class="value">1x1</span>
<button id="map-sub-inc" class="cmd">+</button>
</span>
</div>
</div>
<div class="stack" id="prop-mascara" hidden>
<p class="hint">Una mascara solo TAPA: es un recuadro negro que se
pinta encima de todo, para recortar derrames de luz. No lleva
visual, por eso aqui no hay nada que elegir. Arrastra sus esquinas
en la previsualizacion.</p>
<p class="hint resumen">Si lo que querias era proyectar algo en esta
zona, eso es una <b>capa</b>, no una mascara.</p>
<button id="mask-a-capa" class="cmd accent">Convertirla en capa</button>
</div>
</section>
</div>
</main>
<!-- Ventana de informacion -->
<div id="modal" class="modal" hidden>
<div class="modal-box">
<h2 id="modal-title">Informacion</h2>
<div id="modal-body"></div>
<button id="modal-close" class="cmd accent">Entendido</button>
</div>
</div>
<script src="/lib/socket.io.min.js"></script>
<script src="/lib/codemirror/codemirror.js"></script>
<script src="/lib/codemirror/javascript.js"></script>
<script src="/lib/codemirror/clike.js"></script>
<script src="/panel/panel.js"></script>
</body>
</html>