Carregando...
Carregando...
Um bug (selo de canto) é um elemento pequeno com a marca — indicador AO VIVO, logo do canal, selo de evento — que fica num canto da tela. Ele surge com uma animação de escala e some suavemente.
Posição
Canto superior direito. position: absolute; top: 40px; right: 40px — nunca fixed, que escaparia para a viewport em vez de ficar na área de render de 1920×1080.
Animação
Escala a partir de 50% + blur em vez de deslizar. Um efeito de "materialização" — menos invasivo que um deslize para algo que fica na tela.
Ponto AO VIVO pulsante
Um pulso com @keyframes em CSS, num elemento irmão com posição absoluta, cria o sinal AO VIVO no estilo de broadcast sem nenhum JavaScript.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.bug",
"version": "1.0.0",
"name": "Corner Bug / LIVE",
"description": "Corner indicator that pops in with a scale animation and a pulsing LIVE dot. 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": "LIVE"
},
"sublabel": {
"type": "string",
"title": "Sublabel",
"gddType": "single-line",
"default": "Breaking News"
}
}
}
}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.
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
Em vez de deslizar, o bug cresce a partir de 50% com um blur de 8px. O estado de repouso fica em .bug; o JavaScript só alterna as classes visible e out. Isso cria um efeito sutil de "materialização", menos invasivo que um deslize — perfeito para algo que fica no canto.
.bug {
position: absolute;
top: 40px;
right: 40px;
font-family: 'Inter', system-ui, sans-serif;
transform: scale(0.5);
opacity: 0;
filter: blur(8px);
}
.bug.visible {
transform: scale(1);
opacity: 1;
filter: blur(0);
transition:
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.4s ease,
filter 0.4s ease;
}
.bug.out {
transform: scale(0.8);
opacity: 0;
filter: blur(8px);
transition:
transform 0.4s cubic-bezier(0.76, 0, 0.24, 1),
opacity 0.3s ease,
filter 0.3s ease;
}Dica de design
A animação de entrada é um ease-out de 0.6s que assenta suavemente; a de saída (.bug.out) só encolhe até 80% (não 50%) num ease-in-out mais curto, de 0.4s, com a opacidade e o blur sumindo em 0.3s. Essa assimetria — entrada suave, saída rápida — parece natural. O olho percebe a entrada, mas mal registra a saída.
Mesma estrutura do lower third: um <link> para a folha de estilo (URL absoluta via import.meta.url), um _initDom() preguiçoso e os seis métodos do ciclo de vida. Nada de customElements.define() no nível do módulo — quem escolhe a tag é o renderizador. Este é o arquivo completo do download:
/**
* OGraf Bug / LIVE — corner indicator with a pulsing LIVE dot.
*
* Designed for the OGraf iframe mount model: loads its stylesheet via a
* <link rel="stylesheet"> tag whose URL is computed from import.meta.url,
* so it resolves wherever the renderer serves the package from.
*
* DOM init is done lazily in _initDom() rather than connectedCallback —
* renderers may instantiate the element without attaching it, and load()
* is always the first method called.
*
* Do NOT call customElements.define() here — the renderer picks the tag.
*/
const STYLE_URL = new URL('./style.css', import.meta.url).href;
const TEMPLATE = `
<link rel="stylesheet" href="${STYLE_URL}">
<div class="bug">
<div class="bug-container">
<div class="bug-live">
<div class="bug-live-ping"></div>
<div class="bug-live-dot"></div>
</div>
<div class="bug-text">
<div class="bug-label"></div>
<div class="bug-sublabel"></div>
</div>
</div>
</div>
`;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* 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);
}
export default class BugGraphic extends HTMLElement {
_initDom() {
if (this._initialized) return;
this.innerHTML = TEMPLATE;
this._root = this.querySelector('.bug');
this._label = this.querySelector('.bug-label');
this._sublabel = this.querySelector('.bug-sublabel');
this._step = undefined;
this._rev = 0;
this._initialized = true;
}
async load({ data } = {}) {
this._initDom();
if (data?.label !== undefined) this._label.textContent = data.label;
if (data?.sublabel !== undefined) this._sublabel.textContent = data.sublabel;
return { statusCode: 200 };
}
// 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 };
}
async stopAction({ skipAnimation } = {}) {
this._initDom();
const rev = ++this._rev;
this._step = undefined;
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(400);
if (rev === this._rev) this._root.classList.remove('visible', 'out');
return { statusCode: 200 };
}
async updateAction({ data } = {}) {
this._initDom();
if (data?.label !== undefined) this._label.textContent = data.label;
if (data?.sublabel !== undefined) this._sublabel.textContent = data.sublabel;
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 ?? ''}` };
}
async dispose() {
this._rev = (this._rev ?? 0) + 1;
this.innerHTML = '';
this._initialized = false;
return { statusCode: 200 };
}
}
resolveTargetStep() segue a especificação: goto se informado; senão, o passo atual (-1 antes do primeiro play) mais delta (padrão 1). O bug tem um passo só, então o primeiro play o coloca no ar no passo 0, e um segundo play o tira do ar e retorna currentStep: undefined.this._rev. O stopAction() só remove .visible depois dos seus 400ms se nenhuma ação mais nova tiver começado, então play → stop → play enviados sem esperar terminam com o grafismo no ar.load() e updateAction() aplicam cada campo que for !== undefined: envie só sublabel para mudar apenas ele, ou uma string vazia para limpá-lo.customAction({ id, payload, skipAnimation }) com um id vindo de customActions no manifesto. O bug não declara nenhuma, então qualquer id recebe { statusCode: 404, statusMessage } — 4xx é a faixa de erro da especificação.O mesmo padrão de pacote OGraf — manifesto, CSS, Web Component. Visual diferente, mesma interoperabilidade.

Lower third
Nome e cargo sobre a imagem

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

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