Cargando...
Cargando...
El marcador es el grafismo fijo en la esquina de toda retransmisión deportiva en directo — muestra los equipos, el resultado, el tiempo y el periodo. Este tutorial cubre el ciclo de vida completo, incluidas las customActions, el mecanismo de OGraf para lanzar eventos visuales puntuales, como el destello de un gol, sin cambiar el paso del grafismo.
customActions
El manifiesto declara una acción personalizada, goal. El renderizador la lanza mediante customAction para que el marcador destelle sin cambiar sus datos ni su paso.
Posición fija
A diferencia de los rótulos inferiores, que entran y salen, el marcador se queda en pantalla todo el partido. Entra una vez y después recibe llamadas parciales a updateAction para el resultado, el reloj y el periodo.
Diseño oscuro y compacto
Una tarjeta pequeña arriba a la izquierda, sobre un fondo oscuro casi opaco con una barra de acento azul. Se lee bien sobre cualquier vídeo: césped iluminado, planos de la grada, repeticiones.
Cuando se marca un gol, el renderizador llama a customAction({ id, payload, skipAnimation }) con id: "goal", uno de los ids declarados en customActions del manifiesto. El grafismo añade la clase goal durante 800 ms y luego la quita. Nada más cambia: el resultado llega por separado mediante updateAction. Un id que el grafismo no conoce recibe un 404, y así es como el renderizador sabe que la acción no está soportada.
// The renderer passes { id, payload, skipAnimation }, where id is one of the
// customActions declared in the manifest.
async customAction({ id, skipAnimation } = {}) {
this._initDom();
if (id === 'goal') {
// The flash is pure animation, so skipping it leaves nothing to do.
if (skipAnimation) return { statusCode: 200 };
this._root.classList.add('goal');
await sleep(800);
this._root.classList.remove('goal');
return { statusCode: 200 };
}
return { statusCode: 404, statusMessage: `Unknown custom action: ${id ?? ''}` };
}Clave: customAction frente a updateAction
updateAction cambia los datos persistentes del grafismo (resultado, tiempo, nombres de los equipos). customAction lanza un evento visual pasajero: reproduce una animación y después el grafismo vuelve a su estado visual anterior. Con skipAnimation, como el destello es pura animación, no queda nada que hacer y simplemente retorna.
El manifiesto dice stepCount: 1. resolveTargetStep aplica la regla de la especificación: goto si se indica; si no, el paso actual (-1 antes del primer play) más delta, que por defecto vale 1. El primer play cae en el paso 0 y ejecuta la entrada de 600 ms. Un segundo play apunta al paso 1, que está más allá del último paso, así que el grafismo va a su final: ejecuta stopAction y devuelve currentStep: undefined.
/**
* The step a playAction() lands on, exactly as the spec defines it: `goto`
* wins; otherwise the current step (-1 before the first play) plus `delta`,
* which defaults to 1. A target at or past stepCount means "go to the end",
* returned as undefined — so a second play on a one-step graphic takes it off
* air instead of replaying it.
*/
function resolveTargetStep(currentStep, { goto, delta } = {}, stepCount = 1) {
const target = Number.isInteger(goto) && goto >= 0
? goto
: (currentStep ?? -1) + (Number.isInteger(delta) ? delta : 1);
return target >= stepCount ? undefined : Math.max(target, 0);
}
// 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({ goto, delta, skipAnimation } = {}) {
this._initDom();
const target = resolveTargetStep(this._step, { goto, delta });
if (target === undefined) {
await this.stopAction({ skipAnimation });
return { statusCode: 200, currentStep: undefined };
}
++this._rev;
this._step = target;
this._root.classList.remove('out');
if (skipAnimation) {
this._root.classList.add('instant', 'visible');
return { statusCode: 200, currentStep: this._step };
}
this._root.classList.remove('instant');
// Force a reflow so the browser registers the starting state.
void this._root.offsetWidth;
this._root.classList.add('visible');
await sleep(600);
return { statusCode: 200, currentStep: this._step };
}Cada acción toma el siguiente this._rev. stopAction solo quita la clase visible si no ha empezado ninguna acción más reciente durante su salida de 400 ms, así que play → stop → play enviados sin esperar terminan en antena.
El destello es una única animación de keyframes sobre la tarjeta interior. Mantiene la sombra normal de la tarjeta y hace crecer un resplandor azul a su alrededor, que alcanza su máximo a mitad de camino y luego se desvanece. Su duración de 0.8 s coincide con los 800 ms que espera el grafismo antes de quitar la clase.
/* Goal flash animation */
.score-bug.goal .score-bug-inner {
animation: goalFlash 0.8s ease;
}
@keyframes goalFlash {
0% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 0 0 rgba(37, 99, 235, 0.6); }
25% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 30px 8px rgba(37, 99, 235, 0.5); }
50% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 40px 12px rgba(37, 99, 235, 0.3); }
100% { box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 0 0 rgba(37, 99, 235, 0); }
}Tras cada load y update, el equipo con más goles recibe la clase active; en caso de empate, ninguno. Las actualizaciones pueden ser parciales, así que una que solo toca el reloj no trae resultado: el grafismo usa entonces el resultado que ya está en pantalla en lugar de quitar el resaltado.
_updateActiveTeam(data) {
const homeTeam = this.querySelector('.home');
const awayTeam = this.querySelector('.away');
homeTeam.classList.remove('active');
awayTeam.classList.remove('active');
// A partial update may carry only one score, or none: fall back to what is
// on screen so the leader's highlight survives a clock-only update.
const h = Number(data?.homeScore ?? this._homeScore.textContent);
const a = Number(data?.awayScore ?? this._awayScore.textContent);
if (!Number.isNaN(h) && !Number.isNaN(a)) {
if (h > a) homeTeam.classList.add('active');
else if (a > h) awayTeam.classList.add('active');
}
}.score-team.active .score-team-name {
color: #ffffff;
}
.score-team.active .score-value {
color: #60a5fa;
}Consejo de diseño
El resultado del equipo que va ganando pasa a azul claro y su nombre a blanco puro. Es una señal pequeña, habitual en las retransmisiones deportivas premium, que le dice al espectador quién va por delante de un vistazo sin añadir nada al diseño. Cuando el resultado cambia, updateAction además le da al número un salto de 350 ms con la clase updating.
Fíjate en el array customActions: así declara OGraf operaciones propias del grafismo más allá de play, update y stop. El renderizador solo envía ids que figuren ahí, y el grafismo responde a cualquier otro con un 4xx (este usa 404).
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.score-bug",
"version": "1.0.0",
"name": "Score Bug",
"description": "Persistent on-screen scoreboard with a goal customAction that flashes the bug when a team scores. Tutorial from ograf.dev.",
"author": {
"name": "ograf.dev",
"url": "https://ograf.dev"
},
"main": "graphic.mjs",
"stepCount": 1,
"supportsRealTime": true,
"supportsNonRealTime": false,
"thumbnails": [
{
"file": "thumbnail.webp",
"resolution": {
"width": 1920,
"height": 1080
}
}
],
"customActions": [
{
"id": "goal",
"name": "Goal",
"description": "Flash the scoreboard to celebrate a goal.",
"schema": null
}
],
"schema": {
"type": "object",
"properties": {
"home": {
"type": "string",
"title": "Home Team",
"gddType": "single-line",
"default": "BAR"
},
"away": {
"type": "string",
"title": "Away Team",
"gddType": "single-line",
"default": "RMA"
},
"homeScore": {
"type": "integer",
"title": "Home Score",
"minimum": 0,
"default": 2
},
"awayScore": {
"type": "integer",
"title": "Away Score",
"minimum": 0,
"default": 1
},
"time": {
"type": "string",
"title": "Match Time",
"gddType": "single-line",
"default": "67:42"
},
"period": {
"type": "string",
"title": "Period",
"gddType": "single-line",
"default": "2nd Half"
}
}
}
}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.
score-bug.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
Posición fija, actualizaciones en directo con updateAction y destellos de gol pasajeros con customAction — todo lo necesario para deporte en directo.

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

Cuenta atrás
Reloj que avanza solo

Última hora
Alerta urgente a pantalla completa

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

Tarjeta de redes sociales
Publicación sobreimpresa con avatar