Cargando...
Cargando...
La alerta de última hora es una interrupción a pantalla completa — una capa roja y llamativa que exige atención. A diferencia de otros grafismos, usa stepCount: 0, es decir, es de tipo "lanzar y olvidar": un play la pone en antena, se mantiene y se retira sola sin que el operador tenga que sacarla.
Lanzar y olvidar
stepCount: 0 en el manifiesto. El grafismo entra, se mantiene 3.5 s y se retira solo. No hace falta que ningún operador lo saque, aunque un stop puede cortarlo antes.
Capa a pantalla completa
Llena toda la caja del grafismo con un fondo negro al 85% y desenfocado. La etiqueta roja "Breaking News" y el titular quedan centrados encima.
Aparición escalonada
Una sola clase visible controla tres elementos con distintos retardos de transición: la etiqueta, luego el titular y luego la línea de acento.
Los tiempos están en tres constantes: 1.2 s para que termine la entrada, 3.5 s en antena y 0.6 s para la salida.
// In: the staggered reveal ends when the accent line lands (0.6s delay + 0.6s
// transition). Hold: time on air. Out: the 0.6s fade.
const IN_MS = 1200;
const HOLD_MS = 3500;
const OUT_MS = 600;playAction añade la clase visible y solo espera a la entrada. Después arranca _autoDismiss sin esperarlo y devuelve currentStep: undefined. La permanencia y la salida siguen en segundo plano, y el renderizador puede enviar su siguiente acción en cuanto la alerta está en pantalla.
// Each action takes the next revision number. Anything that finishes after a
// newer action has started checks it and backs off, so play → stop → play
// sent without waiting ends on air instead of hidden by the stale stop.
async playAction({ skipAnimation } = {}) {
this._initDom();
const rev = ++this._rev;
this._root.classList.remove('out');
if (skipAnimation) {
this._root.classList.add('instant');
} else {
this._root.classList.remove('instant');
void this._root.offsetWidth;
}
this._root.classList.add('visible');
// stepCount is 0: the graphic runs start → end on its own. The promise
// resolves once it is on screen, so the renderer is free for the next
// action; the hold and the exit carry on in the background.
if (!skipAnimation) await sleep(IN_MS);
this._autoDismiss(rev, skipAnimation);
return { statusCode: 200, currentStep: undefined };
}
async _autoDismiss(rev, skipAnimation) {
await sleep(HOLD_MS);
if (rev !== this._rev) return;
if (skipAnimation) {
this._root.classList.remove('visible', 'out');
return;
}
this._root.classList.add('out');
await sleep(OUT_MS);
if (rev === this._rev) this._root.classList.remove('visible', 'out');
}Clave: stepCount: 0
stepCount: 0 le dice al renderizador que el grafismo no tiene pasos propios: entra y termina solo. El renderizador sigue llamando a playAction, que debe devolver currentStep: undefined. La especificación dice que la promesa debe resolverse cuando el grafismo esté listo para la siguiente acción, así que se resuelve tras la entrada (IN_MS), no tras toda la permanencia y la salida.
"Lanzar y olvidar" no significa que no se pueda parar. El renderizador puede llamar a stopAction para retirar la alerta antes de que acabe la permanencia, y updateAction puede cambiar el titular mientras está en antena. Cada acción toma el siguiente this._rev. El _autoDismiss en segundo plano comprueba ese número tras la permanencia y otra vez tras la salida, así que una retirada pendiente de un play anterior nunca oculta uno más reciente: play → stop → play enviados sin esperar terminan en antena. customAction tiene que existir aunque el manifiesto no declare ninguna, y responde a cualquier id con 404.
async stopAction({ skipAnimation } = {}) {
this._initDom();
const rev = ++this._rev;
if (skipAnimation) {
this._root.classList.add('instant');
this._root.classList.remove('visible', 'out');
return { statusCode: 200 };
}
this._root.classList.remove('instant');
this._root.classList.add('out');
await sleep(600);
if (rev === this._rev) this._root.classList.remove('visible', 'out');
return { statusCode: 200 };
}
// Every OGraf graphic must expose customAction, even without any declared.
// The renderer passes { id, payload, skipAnimation }; an unknown id is a 4xx.
async customAction({ id } = {}) {
return { statusCode: 404, statusMessage: `Unknown custom action: ${id ?? ''}` };
}Toda la capa aparece con un fundido de 0.4 s. Dentro, la etiqueta crece desde el centro tras 0.2 s, el titular sube a su sitio tras 0.4 s y la línea de acento se dibuja tras 0.6 s: todo lo arranca la misma clase visible, escalonado solo con transition-delay. La línea de acento es la última en terminar, a los 1.2 s, y de ahí sale IN_MS. Al salir, las reglas de out pliegan la etiqueta y la línea y lo desvanecen todo en 0.6 s.
.breaking.visible {
opacity: 1;
transition: opacity 0.4s ease;
}
.breaking.visible .breaking-badge {
transform: scaleX(1);
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.2s;
}
.breaking.visible .breaking-headline {
transform: translateY(0);
opacity: 1;
transition:
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1) 0.4s,
opacity 0.5s ease 0.4s;
}
.breaking.visible .breaking-line {
transform: scaleX(1);
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.6s;
}.breaking-badge-dot {
width: 8px;
height: 8px;
background: #ffffff;
border-radius: 50%;
animation: badgePulse 1.5s ease infinite;
}
@keyframes badgePulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}Consejo de diseño
El punto blanco que late en la etiqueta roja transmite urgencia sin distraer. Es una convención televisiva sutil pero eficaz: el espectador asocia ese punto parpadeante con contenido importante y en directo. La animación es lenta (un ciclo de 1.5 s) para que no resulte frenética.
Fíjate en stepCount: 0: así declara OGraf un grafismo que entra y termina por sí solo. El renderizador sigue llamando a playAction, que devuelve currentStep: undefined en cuanto termina la entrada.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.breaking-news",
"version": "1.0.0",
"name": "Breaking News",
"description": "Fire-and-forget full-screen breaking news overlay that plays in, holds, and auto-dismisses. Tutorial from ograf.dev.",
"author": {
"name": "ograf.dev",
"url": "https://ograf.dev"
},
"main": "graphic.mjs",
"stepCount": 0,
"supportsRealTime": true,
"supportsNonRealTime": false,
"thumbnails": [
{
"file": "thumbnail.webp",
"resolution": {
"width": 1920,
"height": 1080
}
}
],
"schema": {
"type": "object",
"properties": {
"headline": {
"type": "string",
"title": "Headline",
"gddType": "single-line",
"default": "City council approves new riverside transit line"
}
}
}
}Un paquete OGraf Graphics Definition v1 real. Un renderizador compatible lee el manifiesto y gestiona el ciclo de vida. Con licencia MIT; úsalo en cualquier sistema compatible con OGraf.
breaking-news.ograf.json
Manifiesto — lo que lee el renderizador (id, schema, flags del ciclo de vida)
graphic.mjs
Web Component con load / play / update / stop / customAction / dispose
style.css
Hoja de estilos, cargada por graphic.mjs mediante una etiqueta <link>
thumbnail.webp
Vista previa de 1920×1080, declarada en el manifiesto
README.md
Notas de uso
LICENSE
MIT
Despliégalo en un renderizador OGraf compatible
Lanzar y olvidar con stepCount: 0, una aparición escalonada y una retirada automática en segundo plano que un stop todavía puede adelantar.

Rótulo inferior
Nombre y cargo sobreimpresos

Mosca / EN DIRECTO
Indicador de esquina con pulso

Ticker de noticias
Titulares en desplazamiento

Cita a pantalla completa
Tipografía cinematográfica a pantalla completa

Barras electorales
Gráfico de porcentajes animado

Alineación deportiva
Cuadrícula con la plantilla del equipo

Marcador
Marcador de partido en directo

Cuenta atrás
Reloj que avanza solo

Previsión meteorológica
Condiciones actuales y previsión a 3 días

Tarjeta de redes sociales
Publicación sobreimpresa con avatar