Carregando...
Carregando...
O placar é o grafismo fixo no canto de toda transmissão esportiva ao vivo — mostra os times, o placar, o tempo e o período. Este tutorial cobre o ciclo de vida completo, incluindo as customActions, o mecanismo do OGraf para disparar eventos visuais pontuais, como o destaque de um gol, sem mudar o passo do grafismo.
customActions
O manifesto declara uma ação customizada, goal. O renderizador a dispara via customAction para fazer o placar piscar sem mudar seus dados nem seu passo.
Posição fixa
Diferente das lower thirds (tarjas), que entram e saem, o placar fica na tela durante toda a partida. Ele entra uma vez e depois recebe chamadas parciais de updateAction para o placar, o relógio e o período.
Visual escuro e compacto
Um cartão pequeno no canto superior esquerdo, com fundo escuro quase opaco e uma barra de destaque azul. Continua legível sobre qualquer vídeo: gramado claro, planos da torcida, replays.
Quando sai um gol, o renderizador chama customAction({ id, payload, skipAnimation }) com id: "goal", um dos ids declarados em customActions no manifesto. O grafismo adiciona a classe goal por 800 ms e depois a remove. Nada mais muda: o placar em si chega separadamente via updateAction. Um id que o grafismo não conhece recebe um 404, e é assim que o renderizador descobre que a ação não é suportada.
// 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 ?? ''}` };
}Ponto-chave: customAction vs updateAction
updateAction altera os dados persistentes do grafismo (placar, tempo, nomes dos times). customAction dispara um evento visual passageiro: toca uma animação e depois o grafismo volta ao estado visual anterior. Com skipAnimation, como o destaque é pura animação, não sobra nada a fazer e a ação simplesmente retorna.
O manifesto diz stepCount: 1. resolveTargetStep aplica a regra da especificação: goto, se informado; senão, o passo atual (-1 antes do primeiro play) mais delta, que por padrão é 1. O primeiro play cai no passo 0 e roda a entrada de 600 ms. Um segundo play mira o passo 1, que fica além do último passo, então o grafismo vai para o fim: roda stopAction e retorna 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 ação pega o próximo this._rev. stopAction só remove a classe visible se nenhuma ação mais nova tiver começado durante a saída de 400 ms, então play → stop → play enviados sem esperar terminam no ar.
O destaque é uma única animação de keyframes no cartão interno. Ela mantém a sombra normal do cartão e faz crescer um brilho azul ao redor, que atinge o pico na metade e depois some. A duração de 0.8 s bate com os 800 ms que o grafismo espera antes de remover a classe.
/* 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); }
}Depois de cada load e update, o time com mais gols recebe a classe active; no empate, nenhum recebe. Os updates podem ser parciais, então um update só do relógio não traz placar: nesse caso o grafismo usa o placar que já está na tela em vez de tirar o destaque.
_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;
}Dica de design
O placar de quem está na frente fica azul-claro e o nome fica branco puro. É um detalhe pequeno, comum em transmissões esportivas premium, que mostra ao espectador quem está ganhando num relance, sem acrescentar nada ao layout. Quando o placar muda, updateAction também dá ao número um pulo de 350 ms com a classe updating.
Repare no array customActions: é assim que o OGraf declara operações específicas do grafismo além de play, update e stop. O renderizador só envia ids listados ali, e o grafismo responde a qualquer outro com um 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"
}
}
}
}Um pacote OGraf Graphics Definition v1 de verdade. Um renderizador compatível lê o manifesto e conduz o ciclo de vida. Licença MIT; coloque em qualquer sistema compatível com OGraf.
score-bug.ograf.json
Manifesto — o que o renderizador lê (id, schema, flags de ciclo de vida)
graphic.mjs
Web Component com load / play / update / stop / customAction / dispose
style.css
Folha de estilo, carregada pelo graphic.mjs com uma tag <link>
thumbnail.webp
Prévia em 1920×1080, declarada no manifesto
README.md
Instruções de uso
LICENSE
MIT
Posição fixa, atualizações ao vivo via updateAction e destaques passageiros de gol via customAction — o kit completo para esporte ao vivo.

Lower third
Nome e cargo sobre a imagem

Bug / AO VIVO
Indicador de canto com pulso

Ticker de notícias
Manchetes rolando na tela

Citação em tela cheia
Tipografia cinematográfica em tela cheia

Barras de eleição
Gráfico de porcentagens animado

Escalação esportiva
Grade com o elenco do time

Contagem regressiva
Relógio que avança sozinho

Plantão
Alerta urgente em tela cheia

Previsão do tempo
Condições atuais e previsão de 3 dias

Card de rede social
Post sobreposto com avatar