Carregando...
Carregando...
Contagens regressivas estão por toda parte na TV — antes do programa começar, para cronometrar blocos, em leilões e em eventos. Este grafismo é especial porque conta o tempo sozinho usando setInterval — depois que começa, não precisa de nenhuma chamada de update externa.
Conta sozinho
Usa setInterval internamente. Depois de entrar, faz a contagem regressiva por conta própria. O renderizador não precisa fazer nenhuma chamada de updateAction.
Estado de urgência
Quando faltam 10 segundos ou menos, os dígitos ficam vermelhos e pulsam, sinalizando urgência ao espectador sem nenhuma intervenção do operador.
Encerramento limpo
O intervalo precisa ser limpo tanto em stopAction() quanto em dispose(). Esquecer qualquer um deles deixa um timer fantasma rodando em segundo plano.
_startTicking limpa qualquer intervalo anterior e inicia um novo, de 1 segundo. A cada tique, decrementa _remaining, redesenha o relógio e alterna a classe urgent aos 10 segundos. Ao chegar a zero, para sozinho e deixa 00:00 na tela. _paintTime só mexe no span de minutos ou de segundos cujo texto realmente mudou, e _swap reinicia a animação tick desse span com um reflow forçado.
_startTicking() {
this._stopTicking();
this._interval = setInterval(() => {
if (this._remaining <= 0) {
this._stopTicking();
return;
}
this._remaining--;
this._paintTime(this._remaining, { animate: true });
if (this._remaining <= 10) this._root.classList.add('urgent');
else this._root.classList.remove('urgent');
}, 1000);
}
_stopTicking() {
if (this._interval) {
clearInterval(this._interval);
this._interval = null;
}
}
_paintTime(totalSeconds, { animate } = {}) {
const mins = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
const secs = String(totalSeconds % 60).padStart(2, '0');
if (this._mins.textContent !== mins) this._swap(this._mins, mins, animate);
if (this._secs.textContent !== secs) this._swap(this._secs, secs, animate);
}
_swap(el, next, animate) {
if (!animate) {
el.textContent = next;
return;
}
el.classList.remove('tick');
void el.offsetWidth;
el.textContent = next;
el.classList.add('tick');
}load mostra o tempo inicial a partir do campo seconds; o relógio só começa a andar no play. playAction segue o modelo de passos da especificação (goto, senão o passo atual mais delta). Com stepCount: 1, um segundo play passa do último passo, então ele para o grafismo e retorna currentStep: undefined. O intervalo só começa depois da entrada de 800 ms, e só se this._rev não tiver avançado: um stop enviado durante a entrada não pode ser seguido de um relógio que começa a contar mesmo assim. updateAction aceita dados parciais; se o relógio estava rodando, ele recomeça a partir do novo valor.
// 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 };
}
const rev = ++this._rev;
this._step = target;
this._root.classList.remove('out');
if (skipAnimation) {
this._root.classList.add('instant', 'visible');
this._startTicking();
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(800);
if (rev === this._rev) this._startTicking();
return { statusCode: 200, currentStep: this._step };
}
async updateAction({ data } = {}) {
this._initDom();
const wasTicking = this._interval !== null;
this._applyData(data);
if (data?.seconds !== undefined) this._root.classList.remove('urgent');
if (wasTicking) this._startTicking();
return { statusCode: 200 };
}stopAction limpa o intervalo antes da saída de 500 ms, e dispose limpa de novo e incrementa a revisão, para que nada pendente mexa no elemento já esvaziado.
async stopAction({ skipAnimation } = {}) {
this._initDom();
const rev = ++this._rev;
this._step = undefined;
this._stopTicking();
if (skipAnimation) {
this._root.classList.add('instant');
this._root.classList.remove('visible', 'out', 'urgent');
return { statusCode: 200 };
}
this._root.classList.remove('instant');
this._root.classList.add('out');
await sleep(500);
if (rev === this._rev) this._root.classList.remove('visible', 'out', 'urgent');
return { statusCode: 200 };
}
async dispose() {
this._rev = (this._rev ?? 0) + 1;
this._stopTicking();
this.innerHTML = '';
this._initialized = false;
return { statusCode: 200 };
}Dica de design
Sempre limpe os intervalos em stopAction() e dispose(). Num ambiente de broadcast, grafismos são carregados e descarregados o tempo todo. Um intervalo esquecido é um timer rodando em segundo plano, consumindo CPU e podendo causar comportamentos inesperados quando o grafismo é recarregado.
Cada par de dígitos que muda desliza para cima até o lugar em 0.36 s com a classe tick. Quando entra a urgência, o tempo fica vermelho e pulsa suavemente de tamanho uma vez por segundo.
/* Pulse animation for last 10 seconds */
.countdown.urgent .countdown-time {
animation: urgentPulse 1s ease infinite;
color: #dc2626;
}
@keyframes urgentPulse {
0% { transform: scale(1); }
50% { transform: scale(1.04); }
100% { transform: scale(1); }
}
/* Digit swap: the new digit rises into place from below as it fades in */
.countdown-mins.tick,
.countdown-secs.tick {
animation: digitSwap 0.36s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes digitSwap {
0% { transform: translateY(0.55em); opacity: 0; }
40% { opacity: 1; }
100% { transform: translateY(0); opacity: 1; }
}{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.countdown",
"version": "1.0.0",
"name": "Countdown Timer",
"description": "Self-ticking countdown clock that goes red in the last 10 seconds. 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
}
}
],
"schema": {
"type": "object",
"properties": {
"label": {
"type": "string",
"title": "Label",
"gddType": "single-line",
"default": "STARTING IN"
},
"seconds": {
"type": "integer",
"title": "Seconds",
"default": 120,
"minimum": 1
}
}
}
}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.
countdown.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
Contagem própria com setInterval, um estado de urgência e limpeza correta em stopAction() e dispose(): um grafismo de timer autossuficiente.

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

Placar
Placar de partida ao vivo

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