Panel: fuente Xirod, titulos centrados y diseno de dos columnas

Anade la fuente Xirod (web/panel/fonts/xirod.otf, de 1001fonts) para el titulo del panel, tanto en Raspberry como en portatil. Titulos centrados en la cabecera y en cada tarjeta. En pantalla ancha el panel pasa a dos columnas para aprovechar el espacio del portatil (Visuales/Audio/Sensibilidad a la izquierda, Motor y controles a la derecha); en movil sigue en una sola columna, igual que antes. El README explica el uso en ordenador con diagrama, comandos y tabla de diferencias, recalcando que en ordenador no se usa el modo kiosko.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hacklab 2026-05-22 16:28:17 +02:00
parent c8b51615ba
commit 7ebb593288
5 changed files with 281 additions and 182 deletions

View file

@ -2,11 +2,15 @@
# FOSFENO
**Motor de visuales audio-reactivas para Raspberry Pi.**
**Motor de visuales audio-reactivas para Raspberry Pi y para portátil Linux.**
Convierte una Raspberry Pi + un proyector + un micro USB en una estación de
VJ automática: la Pi escucha la música de la sala, detecta su BPM y proyecta
VJ automática: escucha la música de la sala, detecta su BPM y proyecta
visuales que reaccionan al sonido. Todo se controla desde un panel web.
Funciona en dos escenarios: en una **Raspberry Pi** como aparato dedicado que
arranca solo, o en un **portátil Linux** que lanzas cuando quieras. El apartado
[En un portátil Linux](#en-un-portátil-linux) explica las diferencias.
```
[ Música en la sala ]
│ (micro USB)
@ -76,13 +80,7 @@ bash install.sh --no-projectm # omite la compilación de projectM
bash install.sh --check # solo comprueba el sistema, no instala nada
```
### En un portátil Linux
FOSFENO también corre en un portátil con Debian, Ubuntu o Mint, sin Raspberry
Pi. Se instala con `bash install.sh --laptop` y se arranca cuando quieras con
`./fosfeno`. Los detalles están en [FOSFENO en un portátil](docs/portatil.md).
Después:
Después, en la Raspberry, activa el arranque al escritorio:
```bash
sudo raspi-config # System Options → Boot/Auto Login → Desktop Autologin
@ -91,6 +89,49 @@ sudo reboot
Al reiniciar, la Pi arranca sola en modo kiosko mostrando las visuales.
### En un portátil Linux
FOSFENO no necesita la Raspberry: corre igual en un portátil con Debian,
Ubuntu o Mint. El portátil ya trae micrófono y cámara, y lo conectas al
proyector por HDMI como cualquier otra cosa.
```
[ Música de la sala ]
│ (micrófono integrado, o USB)
┌────────────────────────┐
│ Portátil Linux │── HDMI ──▶ [ Proyector :: visuales ]
│ ./fosfeno │
└───────────┬────────────┘
Panel: http://localhost:8080/
(o desde el móvil, en la misma red Wi-Fi)
```
Se instala una vez y se arranca a mano cuando lo necesites:
```bash
bash install.sh --laptop # instala, sin tocar el arranque del sistema
./fosfeno # arranca FOSFENO; Ctrl+C para cerrarlo
```
Diferencias con la Raspberry Pi:
| | Raspberry Pi | Portátil Linux |
|---|---|---|
| Arranque | Automático al encender | A mano, con `./fosfeno` |
| Modo kiosko | Sí: visuales a pantalla completa al arrancar | **No**: ventana normal que mueves al proyector |
| Puerto del panel | 80 — `http://fosfeno.local/` | 8080 — `http://localhost:8080/` |
| Micrófono y cámara | Por USB | Los integrados del portátil |
| Cambios en el sistema | Arranque automático y nombre de red | Ninguno |
La diferencia clave: en el portátil **no se usa el modo kiosko**. Las visuales
salen en una ventana de navegador normal que arrastras a la pantalla del
proyector y pones a pantalla completa con F11. Así FOSFENO no se apodera de tu
pantalla ni se mete en el arranque del sistema; lo abres y lo cierras tú.
Guía completa: [FOSFENO en un portátil](docs/portatil.md).
## Uso
- **Visuales** → salen automáticamente por el proyector (HDMI).

View file

@ -0,0 +1,9 @@
Fuente del panel
================
xirod.otf - fuente "Xirod", usada para el titulo del panel.
Descargada de 1001fonts.com. Comprueba sus condiciones de uso si vas a
distribuir el proyecto con fines comerciales.
Para cambiar la fuente del titulo, sustituye este archivo y ajusta la
regla @font-face de web/panel/panel.css.

BIN
web/panel/fonts/xirod.otf Normal file

Binary file not shown.

View file

@ -10,7 +10,7 @@
</head>
<body>
<header>
<h1>F O S F E N O</h1>
<h1>FOSFENO</h1>
<span id="conn" class="dot off" title="Conexion"></span>
</header>
@ -20,167 +20,176 @@
<button id="notif-close" aria-label="Cerrar aviso">&times;</button>
</div>
<!-- En movil las tarjetas van en una sola columna; en pantalla ancha,
en dos columnas (.column pasa a display:contents en movil). -->
<main>
<!-- Encendido -->
<section class="card center">
<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>
<div class="column">
<!-- Selector de motor -->
<section class="card">
<div class="cardhead">
<span class="label">Motor de visuales</span>
<button class="info" data-help="engines">i</button>
</div>
<div class="engines">
<button class="engine" data-engine="projectm">
<strong>projectM</strong><small>nativo</small></button>
<button class="engine" data-engine="butterchurn">
<strong>Butter</strong><small>MilkDrop</small></button>
<button class="engine" data-engine="hydra">
<strong>Hydra</strong><small>codigo</small></button>
<button class="engine" data-engine="shaders">
<strong>Shaders</strong><small>GLSL</small></button>
<button class="engine" data-engine="mixer">
<strong>Mezcla</strong><small>camara</small></button>
</div>
</section>
<!-- 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">
<div class="cardhead">
<span class="label">Audio</span>
<button class="info" data-help="audio">i</button>
</div>
<select id="audio-device"><option>Detectando entradas...</option></select>
<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">Si conectas el microfono o la camara con la Raspberry ya
encendida, pulsa este boton para que aparezcan.</p>
</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>
<select id="audio-device"><option>Detectando entradas...</option></select>
<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">Si conectas el microfono o la camara con el equipo ya
encendido, pulsa este boton para que aparezcan.</p>
</section>
<!-- Sensibilidad -->
<section class="card">
<div class="cardhead">
<span class="label">Sensibilidad al audio:
<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>
<!-- Sensibilidad -->
<section class="card" id="card-sens">
<div class="cardhead">
<span class="label">Sensibilidad al audio:
<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>
<!-- 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>
<!-- Estado -->
<section class="card" id="card-now">
<span class="label">Ahora suena</span>
<div id="now" class="now">-</div>
</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>
<!-- Sistema -->
<section class="card sys" id="card-sys">
<button id="reboot" class="sysbtn">Reiniciar</button>
<button id="shutdown" class="sysbtn danger">Apagar</button>
</section>
<!-- Mezclador VJ -->
<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>
<div class="seg" id="mixer-source">
<button class="segbtn" data-src="cam">Camara</button>
<button class="segbtn" data-src="video">Video</button>
<button class="segbtn" data-src="mix">Mezcla</button>
</div>
<label class="check">
<input type="checkbox" id="mix-cam"> Camara activada
</label>
<select id="mix-camera"><option value="0">Camara por defecto</option></select>
<select id="mix-video"><option value="">-- sin video --</option></select>
<button id="mix-rescan" class="cmd">Actualizar lista de videos</button>
<div class="row">
<span class="label">Modo de mezcla</span>
<select id="mix-blend" class="inline">
<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>
<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>
<p id="net-foot" class="netfoot">FOSFENO</p>
</div>
<!-- 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>
<div class="row">
<button class="cmd" data-action="prev">&laquo; Anterior</button>
<button class="cmd" data-action="next">Siguiente &raquo;</button>
</div>
</section>
<div class="column">
<!-- Estado -->
<section class="card">
<span class="label">Ahora suena</span>
<div id="now" class="now">-</div>
</section>
<!-- Selector de motor -->
<section class="card" id="card-engines">
<div class="cardhead">
<span class="label">Motor de visuales</span>
<button class="info" data-help="engines">i</button>
</div>
<div class="engines">
<button class="engine" data-engine="projectm">
<strong>projectM</strong><small>nativo</small></button>
<button class="engine" data-engine="butterchurn">
<strong>Butter</strong><small>MilkDrop</small></button>
<button class="engine" data-engine="hydra">
<strong>Hydra</strong><small>codigo</small></button>
<button class="engine" data-engine="shaders">
<strong>Shaders</strong><small>GLSL</small></button>
<button class="engine" data-engine="mixer">
<strong>Mezcla</strong><small>camara</small></button>
</div>
</section>
<!-- Sistema -->
<section class="card sys">
<button id="reboot" class="sysbtn">Reiniciar Pi</button>
<button id="shutdown" class="sysbtn danger">Apagar Pi</button>
</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>
<p id="net-foot" class="netfoot">FOSFENO</p>
<!-- 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 -->
<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>
<div class="seg" id="mixer-source">
<button class="segbtn" data-src="cam">Camara</button>
<button class="segbtn" data-src="video">Video</button>
<button class="segbtn" data-src="mix">Mezcla</button>
</div>
<label class="check">
<input type="checkbox" id="mix-cam"> Camara activada
</label>
<select id="mix-camera"><option value="0">Camara por defecto</option></select>
<select id="mix-video"><option value="">-- sin video --</option></select>
<button id="mix-rescan" class="cmd">Actualizar lista de videos</button>
<div class="row">
<span class="label">Modo de mezcla</span>
<select id="mix-blend" class="inline">
<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>
<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>
<div class="row">
<button class="cmd" data-action="prev">&laquo; Anterior</button>
<button class="cmd" data-action="next">Siguiente &raquo;</button>
</div>
</section>
</div>
</main>
<!-- Ventana de informacion -->

View file

@ -1,5 +1,13 @@
/* FOSFENO :: Panel de control
Tema verde acido y negro, con acentos en naranja neon. */
Tema verde acido y negro, con acentos en naranja neon.
Movil: una columna. Pantalla ancha: dos columnas. */
@font-face {
font-family: "Xirod";
src: url("/panel/fonts/xirod.otf") format("opentype");
font-display: swap;
}
:root {
--bg: #060a06;
--card: #0d120c;
@ -27,24 +35,52 @@ body {
padding-bottom: 48px;
}
/* --- Cabecera con el titulo centrado --- */
header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 20px; border-bottom: 2px solid var(--green-d);
position: sticky; top: 0; background: var(--bg); z-index: 10;
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; justify-content: center;
padding: 16px 20px; border-bottom: 2px solid var(--green-d);
background: var(--bg);
}
header h1 {
font-size: 20px; font-weight: 800; letter-spacing: 0.32em;
color: var(--green); text-shadow: 0 0 14px rgba(180, 255, 0, 0.55);
font-family: "Xirod", "Arial Black", sans-serif;
font-size: 21px; font-weight: 400; letter-spacing: 0.16em;
color: var(--green); text-shadow: 0 0 16px rgba(180, 255, 0, 0.6);
}
#conn {
position: absolute; right: 20px; top: 50%; transform: translateY(-50%);
}
.dot { width: 13px; height: 13px; border-radius: 50%; display: inline-block; }
.dot.on { background: var(--green); box-shadow: 0 0 9px var(--green); }
.dot.off { background: var(--red); box-shadow: 0 0 9px var(--red); }
/* --- Disposicion: movil en una columna --- */
main {
max-width: 560px; margin: 0 auto;
padding: 16px; display: flex; flex-direction: column; gap: 14px;
}
.column { display: contents; } /* en movil, las columnas no existen */
/* Orden de las tarjetas cuando estan todas en una sola columna (movil) */
#card-power { order: 1; }
#card-engines { order: 2; }
#card-audio { order: 3; }
#card-sens { order: 4; }
#ctl-butterchurn, #ctl-editor, #ctl-mixer, #ctl-projectm { order: 5; }
#card-now { order: 6; }
#card-sys { order: 7; }
#net-foot { order: 8; }
/* --- Disposicion: pantalla ancha (portatil) en dos columnas --- */
@media (min-width: 880px) {
main {
max-width: 1080px; flex-direction: row;
align-items: flex-start; gap: 18px;
}
.column {
display: flex; flex-direction: column; gap: 14px; flex: 1 1 0;
}
}
/* --- Tarjetas --- */
.card {
@ -54,9 +90,10 @@ main {
}
.card.center { align-items: center; }
/* Cabecera de tarjeta: titulo centrado, boton de info a la derecha */
.cardhead {
display: flex; align-items: center; justify-content: space-between;
gap: 10px; width: 100%;
position: relative; width: 100%;
display: flex; align-items: center; justify-content: center;
}
.label { color: var(--green); font-size: 13px; text-transform: uppercase;
@ -68,9 +105,10 @@ main {
gap: 10px; }
.row .cmd { flex: 1; }
/* --- Boton de informacion (circular naranja) --- */
/* --- Boton de informacion (circular naranja, esquina de la tarjeta) --- */
.info {
width: 28px; height: 28px; flex: none; border-radius: 50%;
position: absolute; right: 0; top: 50%; transform: translateY(-50%);
width: 28px; height: 28px; border-radius: 50%;
background: var(--orange); color: var(--ink); border: none;
font-weight: 900; font-style: italic; font-size: 15px; cursor: pointer;
box-shadow: 0 0 10px rgba(255, 122, 0, 0.5);
@ -115,9 +153,7 @@ main {
font-weight: 700; font-size: 14px;
}
.cmd:active, .sysbtn:active { background: #202a16; }
.cmd.accent {
background: var(--green); color: var(--ink); border: none;
}
.cmd.accent { background: var(--green); color: var(--ink); border: none; }
/* --- Selectores y sliders --- */
select {
@ -165,6 +201,8 @@ input[type=range] { width: 100%; accent-color: var(--green); height: 30px; }
font-size: 13px; font-family: "Fira Mono", Consolas, monospace;
}
/* --- Tarjeta de estado --- */
#card-now { text-align: center; }
.now { font-size: 15px; color: var(--orange-n); word-break: break-word; }
/* --- Sistema --- */
@ -172,11 +210,17 @@ input[type=range] { width: 100%; accent-color: var(--green); height: 30px; }
.sys .sysbtn { flex: 1; }
.sysbtn.danger { color: var(--red); border-color: var(--red); }
/* --- Pie con la direccion del panel --- */
.netfoot {
text-align: center; color: var(--dim);
font-size: 12px; font-family: monospace; padding: 4px 0 8px;
}
/* --- Banda de avisos --- */
.notif {
display: flex; align-items: center; gap: 10px;
padding: 12px 16px; font-size: 14px; font-weight: 600;
position: sticky; top: 60px; z-index: 9;
position: sticky; top: 56px; z-index: 9;
}
.notif.info { background: var(--green); color: var(--ink); }
.notif.warn { background: var(--orange); color: var(--ink); }
@ -201,12 +245,8 @@ input[type=range] { width: 100%; accent-color: var(--green); height: 30px; }
box-shadow: 0 0 30px rgba(180, 255, 0, 0.3);
}
.modal-box h2 {
color: var(--green); font-size: 18px; letter-spacing: 0.04em;
font-family: "Xirod", "Arial Black", sans-serif; font-weight: 400;
color: var(--green); font-size: 16px; letter-spacing: 0.06em;
text-align: center;
}
.modal-box p { color: var(--txt); font-size: 14px; line-height: 1.5; }
/* Pie con la direccion del panel */
.netfoot {
text-align: center; color: var(--dim);
font-size: 12px; font-family: monospace; padding: 4px 0 8px;
}