Todas las páginas Crear un entorno

Documentación Crea un espectáculo de luces

Crear un entorno

Escribe tu propio espectáculo de luces: un pequeño paquete de JavaScript que recibe la música y decide qué aspecto tiene.

Un entorno es un paquete que cualquiera puede escribir y que cualquier servidor Rainy puede instalar. Rainy hace la parte difícil: analiza la canción, lleva el compás con el audio y le entrega a tu código una descripción de la música sesenta veces por segundo. Tu código dibuja.

En esta página se construye uno desde cero. La Referencia de la API de entornos enumera todo lo que puedes usar, y Cómo funciona el espectáculo de luces explica de dónde salen los datos.

Antes de empezar

Necesitas:

  • Una copia del repositorio de Rainy que puedas ejecutar en local. Consulta Instalación.
  • Unas cuantas canciones analizadas en su biblioteca.
  • JavaScript. Un entorno son módulos ES sin más: sin paso de compilación ni framework. Usa un canvas 2D, WebGL, el DOM o cualquier biblioteca que lleves dentro del paquete.

Hay dos entornos en el repositorio que conviene tener abiertos junto a esta página:

  • packages/example-pulse: unas 130 líneas sobre un canvas 2D. Todo lo de esta página, en un solo archivo.
  • packages/voyage: un entorno 3D completo con three.js, sus propias letras y un diseño inmersivo.

Iniciar Rainy en modo de desarrollo

Inicia el servidor con una carpeta de paquetes sin empaquetar:

RAINY_DEV=1 RAINY_ENVIRONMENTS_DEV=packages python app.py

Cada carpeta de packages/ que contenga un manifiesto válido aparece ahora como un entorno, con la etiqueta Desarrollo, y se sirve directamente desde el disco, sin caché. Cambia un archivo, recarga la página, y tu cambio ya está en marcha. Las carpetas que no pasan la validación se indican en la consola del servidor, con el motivo.

El modo de desarrollo es para tu propia máquina. También relaja el requisito de la clave de sesión, así que no lo uses nunca en un servidor al que accedan otras personas.

1. El manifiesto

Crea packages/my-show/rainy-environment.json:

{
    "manifest": 1,
    "api": 1,
    "id": "my-show",
    "name": "My Show",
    "version": "0.1.0",
    "description": "One line for the picker",
    "author": "You",
    "entry": "index.js"
}

id es lo que identifica a tu entorno en un servidor: letras minúsculas, dígitos y guiones. api es la versión de la API de entornos para la que escribes; hoy es 1.

2. El entorno más pequeño

Crea packages/my-show/index.js. La exportación por defecto del módulo es una clase con cuatro métodos: mount construye lo que necesitas dentro de ctx.root, tu propia capa; unmount libera lo que la página no puede liberar por ti; update lee la música en cada fotograma, y render dibuja a continuación.

import { TAU, rgba } from '@rainy/lightshow';

export default class MyShow {
    // Rainy hands you ctx. Build what you need inside ctx.root, your own layer.
    mount(ctx) {
        this.canvas = document.createElement('canvas');
        this.canvas.style.cssText = 'display:block;width:100%;height:100%';
        ctx.root.appendChild(this.canvas);
        this.g = this.canvas.getContext('2d');
    }

    // Release what the page cannot. Rainy removes your layer itself.
    unmount() {}

    // Every frame, first: read the music, decide what happens.
    update(S) {}

    // Every frame, second: draw.
    render(S) {
        const { g, canvas } = this;
        if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {
            canvas.width = canvas.clientWidth;
            canvas.height = canvas.clientHeight;
        }
        g.fillStyle = rgba(S.palette.deep, 1);
        g.fillRect(0, 0, canvas.width, canvas.height);

        // A disc that jumps with the kick drum.
        g.fillStyle = rgba(S.palette.a, S.dim);
        g.beginPath();
        g.arc(canvas.width / 2, canvas.height / 2, canvas.height * (0.1 + 0.1 * S.pulse.k), 0, TAU);
        g.fill();
    }
}

Recarga Rainy, abre Ajustes > Espectáculo de luces, elige My Show, reproduce una canción y activa el espectáculo. El disco bombea con el bombo, en el color de la tonalidad de la canción.

@rainy/lightshow es el único módulo de Rainy que importa un paquete. Tiene utilidades de matemáticas, color y tiempo. Todo lo demás llega a través de ctx y S.

3. Usar la música

S es el estado del espectáculo en este fotograma. Las partes a las que recurrirás primero:

S.t El tiempo de la canción en segundos, al compás del audio.
S.pulse.k, .s, .h Bombo, caja y charles, de 0 a 1: suben de golpe con cada golpe y se apagan.
S.F.bands Energía de 0 a 1 en sub, bass, mid, high y air, además de rms (volumen).
S.F.newBeat, S.F.newBar Verdadero en el fotograma en que empieza un pulso o un compás.
S.F.section.label intro, verse, build, chorus, drop, breakdown u outro.
S.F.buildProgress, S.F.dropIn Cuánto se ha avanzado en una subida, y segundos hasta el próximo drop.
S.palette Cinco colores para esta sección: a, b, c, un blanco teñido w y un oscuro deep.
S.I Intensidad de 0,3 a 1: con cuánta fuerza debe ir el espectáculo.
S.dim El atenuador general. Multiplica tu luz por él.

Por ejemplo, un anillo en cada compás que se expande y se apaga:

// in mount(): this.rings = [];

update(S) {
    if (S.F.jumped) this.rings = [];                       // the user seeked: start clean
    if (S.running && S.F.newBar) this.rings.push(S.t);     // remember when, in song time
    this.rings = this.rings.filter((born) => S.t - born < 2 && S.t >= born);
}

render(S) {
    // … after the background …
    const cx = canvas.width / 2, cy = canvas.height / 2;
    for (const born of this.rings) {
        const age = (S.t - born) / 2;                      // 0 when it starts, 1 when it is gone
        g.strokeStyle = rgba(S.palette.b, (1 - age) * S.dim);
        g.lineWidth = 3;
        g.beginPath();
        g.arc(cx, cy, canvas.height * (0.1 + 0.4 * age), 0, TAU);
        g.stroke();
    }
}

Fíjate en que los anillos recuerdan el tiempo de la canción, no un número de fotogramas. Ese es el hábito que hay que adquirir: calcula lo que hay en pantalla a partir de S.t. Así, un salto, una pausa o un fotograma perdido no pueden dejar tu espectáculo descompasado.

Mirar hacia delante

setScore(score) te da la canción entera de una vez: cada pulso, cada golpe de batería, cada sección y la energía de cada banda cincuenta veces por segundo. Así es como Voyage muestra la música que viene como el terreno que tienes delante. Añade el método a tu clase y guarda la partitura:

setScore(score) {
    this.score = score;     // null while a song has no score yet
}

4. Destellos

Nunca hagas estrobos por tu cuenta. Pídeselos al director y dibuja lo que permita:

update(S) {
    for (const ev of S.F.events) {
        if (ev.type === 'drop') S.flash(1, S.palette.w);
    }
}

render(S) {
    // … your scene …
    if (S.flashAmt > 0.01) {
        g.fillStyle = rgba(S.flashColor, 0.6 * S.flashAmt);
        g.fillRect(0, 0, canvas.width, canvas.height);
    }
}

El director limita los destellos a tres por segundo y los convierte en pulsos suaves cuando el usuario tiene activado Reducir destellos. Ese límite protege a las personas con epilepsia fotosensible. Solo funciona si todos los entornos dibujan S.flashAmt en lugar de decidir por su cuenta.

5. Letras

Cuando el usuario tiene las letras en escena, S.lyricsOn es verdadero y S.lyrics te dice qué se canta y cuándo. El aspecto lo decides tú:

update(S) {
    let text = '';
    if (S.lyricsOn) {
        const i = S.lyrics.lineIndex(S.t);
        const line = i >= 0 ? S.lyrics.lines[i] : null;
        if (line && !line.blank) {
            // Each word has a start (s) and an end (e), in song time.
            text = line.words.filter((w) => S.t >= w.s).map((w) => w.text).join(' ');
        }
    }
    this.lineEl.textContent = text;
}

Mantén el texto fuera de los controles del reproductor: ctx.ui.safeArea() indica qué parte de cada lado del escenario cubren.

6. Diseño y botones

Añade cualquiera de estos campos al manifiesto:

{
    "layout": "immersive",
    "requires": ["webgl2"]
}
  • layout es stage (el valor por defecto: el reproductor tal como está, con tu espectáculo detrás) o immersive (tu espectáculo es toda la imagen, y la canción pasa a ser una tarjeta pequeña).
  • requires enumera lo que el dispositivo debe tener: webgl, webgl2 o webgpu. En un dispositivo que no lo tenga, tu entorno no se puede elegir.

Para un control propio, añade un botón a la barra del espectáculo:

this.button = ctx.ui.addButton({
    icon: '<svg viewBox="0 0 24 24" width="14" height="14">…</svg>',
    title: 'More rings',
    onClick: () => { this.many = !this.many; this.button.setPressed(this.many); },
});

7. Cambiar el estilo del reproductor

Tu entorno puede cambiar el aspecto del propio reproductor a pantalla completa: sus colores, sus tamaños, qué se muestra y qué se oculta. Eso se hace con una hoja de estilo, no con JavaScript. Crea packages/my-show/style.css y nómbrala en el manifiesto:

{
    "styles": ["style.css"]
}

Rainy carga la hoja de estilo cuando tu entorno sale a escena y la retira cuando se va, así que nada de lo que escribas sobrevive a tu espectáculo.

Mientras estás en escena, el reproductor tiene la clase ls-env-my-show (ls-env- y tu id). Empieza todas las reglas con ella. La paleta de la canción está disponible como variables CSS, así que el reproductor puede tomar los colores de la música:

/* The progress bar and the play button take the colours of the song. */
#fullscreen-player.ls-env-my-show .fs-progress-fill {
    background-color: rgb(var(--ls-a));
}

#fullscreen-player.ls-env-my-show .fs-control-btn.play-pause {
    background-color: rgb(var(--ls-w));
}

/* A glow around the artwork, in the second colour, at half strength. */
#fullscreen-player.ls-env-my-show .fs-artwork-container {
    box-shadow: 0 0 70px rgb(var(--ls-b) / 0.55);
}

/* Hide the Rainy mark while this show is on. */
#fullscreen-player.ls-env-my-show .fs-brand {
    display: none;
}

--ls-a, --ls-b y --ls-c son los tres colores de la sección, --ls-w es un blanco teñido y --ls-deep un tono oscuro. Cada una contiene r g b, así que escribe rgb(var(--ls-a)), o rgb(var(--ls-a) / 0.5) para una transparencia del 50 %. Se deslizan hacia los colores de la siguiente sección a medida que avanza la canción.

El reproductor también te dice en qué estado está, con clases que puedes combinar con la tuya: ls-idle mientras un diseño inmersivo está atenuado, mode-standard o mode-modern según el modo de pantalla completa que eligió el usuario, ls-lowpower para el Modo de bajo rendimiento. La lista completa de clases, partes y variables está en Dar estilo al reproductor.

Tres límites:

  • Quédate dentro del reproductor a pantalla completa. Tus reglas no deben afectar a nada fuera de #fullscreen-player.ls-env-my-show.
  • Deja siempre una salida. No ocultes ni tapes el botón de cerrar ni el del espectáculo de luces, y mantén utilizables los controles de reproducción.
  • El CSS recibe la paleta, no el pulso. Todo lo que se mueve con la música va en tu capa.

8. Una imagen

La gente elige un entorno por su imagen. Guarda un fotograma de tu espectáculo como imagen 16:9 (960 × 540 es de sobra) y nómbrala en el manifiesto:

{
    "preview": "preview.jpg"
}

Deja fuera la portada y la letra de una canción, salvo que tengas derecho a publicarlas.

9. Empaquetar e instalar

python scripts/pack_environment.py packages/my-show

Esto comprueba la carpeta exactamente igual que un servidor comprueba una subida y escribe dist/my-show-0.1.0.zip. Los archivos ocultos y la carpeta tests/ se dejan fuera. Ese zip es tu entorno: compártelo, y un administrador lo instala en Ajustes > Espectáculo de luces > Entornos instalados. Consulta Instalar un entorno.

Para publicar una versión nueva, sube version en el manifiesto y empaqueta otra vez. Instalarla sobre la anterior la actualiza.

El entorno de ejemplo Pulse en el reproductor a pantalla completa: anillos y un pequeño espectro detrás de la portada y la cola.

Pulse, el ejemplo de packages/example-pulse, como entorno de escenario: dibuja detrás del reproductor, y su hoja de estilo tiñe el reproductor con los colores de la canción; aquí, el resplandor alrededor de la portada.

Reglas de la casa

  • Los destellos pasan por el director. Llama a S.flash y dibuja S.flashAmt.
  • Respeta los ajustes del usuario. S.prefs.reduceFlashing también significa nada de sacudidas de cámara ni movimientos bruscos. Respeta S.prefs.disableLasers, S.prefs.lowPower y el nivel de calidad tier que recibe resize.
  • Sé una función del tiempo de la canción. Calcula a partir de S.t y de la partitura en lugar de acumular estado fotograma a fotograma.
  • Quédate dentro de ctx. Dibuja en ctx.root y habla con Rainy a través de ctx y @rainy/lightshow. No toques los demás elementos de la página ni la API de Rainy: eso es lo que mantiene tu paquete funcionando cuando Rainy cambia, y lo que permitirá a Rainy ejecutar paquetes aislados en el futuro.
  • Lleva tus bibliotecas contigo. Pon dentro del paquete lo que importes. No cargues código de otros servidores.
  • Falla de forma visible. Si no puedes continuar, por ejemplo tras perder el contexto de WebGL, llama a ctx.fail(error). Rainy vuelve a Clásico y el espectáculo continúa.

Si tu código lanza una excepción, Rainy hace lo mismo: retira el entorno del escenario, quita lo que añadió y cambia a Clásico. El error queda en la consola del navegador.

Relacionado