Carregando...
Carregando...
Telejornais mostram posts de redes sociais na tela com frequência — um tweet de uma figura pública, uma nota oficial ou um post viral. Este card mostra o nome do usuário, o @ e o texto do post, com um avatar gerado automaticamente. Ele fica do lado direito da tela, deixando o apresentador visível à esquerda.
Posição à direita
Usa right: 48px; bottom: 80px em vez de left. Assim o apresentador continua visível no lado esquerdo do quadro — uma convenção comum em broadcast para conteúdo exibido.
Avatar gerado automaticamente
Nenhuma imagem necessária. O círculo do avatar mostra as iniciais do usuário, extraídas do nome com _getInitials(). Nada para hospedar, nada para quebrar.
Entrada em dois tempos
O card entra deslizando pela direita, de fora da tela, com um leve blur em 0.7s, e o avatar surge 0.35s depois numa curva que passa do ponto e volta — tudo em CSS, disparado por uma única classe .visible.
Em vez de exigir a URL de uma foto de perfil (que pode quebrar, ter baixa resolução ou problemas de direitos), o card gera um avatar a partir do nome do usuário. _getInitials divide o nome nos espaços, pega a primeira letra de cada palavra, coloca em maiúscula e fica com as duas primeiras: "Jane Smith" → "JS", "Dr. Martin King" → "DM", "Madonna" → "M", e um nome vazio → um avatar vazio. _applyData é compartilhado por load() e updateAction(); ele aplica cada campo que for !== undefined, então uma atualização parcial mexe só nos campos que você envia, e uma string vazia limpa um campo. Ele também aceita uma string opcional platform para o selo escuro no cabeçalho. O schema do manifesto não a declara, então por padrão o selo fica vazio — e .social-platform:empty o esconde em vez de deixar uma cápsula em branco.
_getInitials(name) {
if (!name) return '';
return String(name).split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2);
}
_applyData(data) {
if (!data) return;
if (data.user !== undefined) {
this._userName.textContent = data.user;
this._initials.textContent = this._getInitials(data.user);
}
if (data.handle !== undefined) this._handle.textContent = data.handle;
if (data.text !== undefined) this._text.textContent = data.text;
if (data.platform !== undefined) this._platform.textContent = data.platform;
}
async load({ data } = {}) {
this._initDom();
this._applyData(data);
return { statusCode: 200 };
}Play e stop seguem o modelo de passos do OGraf. O primeiro play coloca o card no ar no passo 0 e resolve depois de 700ms; um segundo play passa do único passo, executa o stop e retorna currentStep: undefined. Toda ação incrementa this._rev, e o stop só remove .visible depois dos seus 500ms se nada mais novo tiver começado — então play → stop → play enviados sem esperar terminam com o card no ar.
/**
* 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(700);
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(500);
if (rev === this._rev) this._root.classList.remove('visible', 'out');
return { statusCode: 200 };
}O card entra deslizando pela borda direita: .social começa em translateX(120%), transparente e com blur, e .social.out o manda de volta em 0.5s. Uma barra em degradê de 4px desenhada com ::before dá ao card branco o destaque azul à esquerda, e o avatar é um círculo azul sólido que cresce a partir de 40%. O reset é limitado com :where(.social-card-root, …), então nunca altera o estilo da página do renderizador.
/* Reset scoped to the graphic. A bare `*` rule in light DOM would also restyle
* the renderer's page and every other graphic on it; :where() keeps the
* specificity at zero, same as the bare selector it replaces. */
:where(.social-card-root, .social-card-root *),
:where(.social-card-root, .social-card-root *)::before,
:where(.social-card-root, .social-card-root *)::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.social {
position: absolute;
bottom: 80px;
right: 48px;
font-family: 'Inter', system-ui, sans-serif;
transform: translateX(120%);
opacity: 0;
filter: blur(4px);
}
.social.visible {
transform: translateX(0);
opacity: 1;
filter: blur(0);
transition:
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.5s ease,
filter 0.5s ease;
}
.social.out {
transform: translateX(120%);
opacity: 0;
filter: blur(4px);
transition:
transform 0.5s cubic-bezier(0.76, 0, 0.24, 1),
opacity 0.4s ease 0.1s,
filter 0.4s ease 0.1s;
}
/* Blue accent bar on left */
.social-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%);
border-radius: 4px 0 0 4px;
}
.social-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: #2563eb;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transform: scale(0.4);
opacity: 0;
transition:
transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.35s,
opacity 0.4s ease 0.35s;
}
.social.visible .social-avatar {
transform: scale(1);
opacity: 1;
}Dica de design
O círculo do avatar usa a primeira letra de cada palavra do nome do usuário — "Jane Smith" vira "JS". Isso dispensa totalmente imagens externas. O preenchimento sólido no azul da marca e a entrada atrasada fazem o círculo parecer proposital, e não um substituto para uma imagem que faltou.
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.social-card",
"version": "1.0.0",
"name": "Social Media Card",
"description": "Social post overlay that slides in from the right with an auto-generated avatar from the user's initials. 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": {
"user": {
"type": "string",
"title": "User",
"gddType": "single-line",
"default": "Jamie Rivera"
},
"handle": {
"type": "string",
"title": "Handle",
"gddType": "single-line",
"default": "@jamie.demo"
},
"text": {
"type": "string",
"title": "Post",
"gddType": "multi-line",
"default": "Just shipped our first OGraf template to production."
}
}
}
}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.
social-card.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 à direita, avatar com iniciais gerado automaticamente e um destaque azul limpo — pronto para exibir posts de redes sociais no ar.

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

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