Carregando...
Carregando...
Um card de citação elegante em tela cheia — do tipo usado para trechos de entrevista, quadros motivacionais ou aberturas editoriais. O texto, o divisor e a atribuição aparecem numa sequência escalonada, com impacto cinematográfico.
A mágica aqui são os delays de transição do CSS. O texto e a atribuição começam transparentes e um pouco abaixo da posição final; o divisor começa com largura zero. Quando a classe .visible é adicionada, eles animam em sequência:
delay: 0.3sTexto da citação
Sobe surgindo de baixo
delay: 0.4sLinha divisória
Cresce a partir do centro
delay: 0.5sAtribuição
Sobe surgindo por último
Tudo fica dentro de .quote-root. O reset é limitado com :where(.quote-root, …), então nunca altera o estilo da página do renderizador, e a raiz ocupa a caixa que o renderizador fornecer com position: absolute; inset: 0. O estado oculto de cada filho (como .quote-text abaixo) fica 20px mais baixo e com opacidade zero; as regras de .visible trazem os delays.
/* 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(.quote-root, .quote-root *),
:where(.quote-root, .quote-root *)::before,
:where(.quote-root, .quote-root *)::after { margin: 0; padding: 0; box-sizing: border-box; }
/* The graphic's own root. `position: absolute; inset: 0` fills whatever box the
* renderer hands us: in an iframe mount that is the frame, and inside a shadow
* root or a positioned container it is that container. `position: fixed` would
* escape to the browser viewport in the second case. The package never styles
* `body` — the renderer owns the document, not the graphic. */
.quote-root {
position: absolute;
inset: 0;
overflow: hidden;
}
.quote {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
}
.quote.visible {
opacity: 1;
transition: opacity 0.8s ease;
}
.quote.visible .quote-text {
opacity: 1;
transform: translateY(0);
transition: opacity 0.6s ease 0.3s, transform 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.3s;
}
.quote.visible .quote-line {
transform: scaleX(1);
transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.4s;
}
.quote.visible .quote-attr {
opacity: 1;
transform: translateY(0);
transition: opacity 0.6s ease 0.5s, transform 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.5s;
}
.quote-text {
font-family: 'Instrument Serif', serif;
font-size: 48px;
line-height: 1.3;
color: white;
font-style: italic;
opacity: 0;
transform: translateY(20px);
}Dica de design
O fundo começa em scale(1.1) e faz a transição para scale(1). Isso cria um efeito sutil de "câmera assentando" — o fundo se ajusta suavemente enquanto a citação aparece. Clima de cinema com uma linha de CSS.
Este template usa duas fontes para criar contraste:
"A citação"
Instrument Serif — itálico, grande (48px). A voz editorial e elegante.
A atribuição
FUNÇÃO / CARGO
Inter — sans-serif limpa e moderna. A voz factual.
Como a animação escalonada demora mais que um simples deslize, a promise de playAction() espera 1300ms antes de resolver — a duração da revelação mais lenta. A essa altura o fundo (1s), o texto da citação (0.3s de delay + 0.8s), o divisor (0.4s + 0.6s) e a atribuição (0.5s + 0.8s) já chegaram ao lugar. stopAction() faz o card inteiro sumir em 500ms.
O resto segue o modelo de passos do OGraf. resolveTargetStep() usa goto se informado; senão, o passo atual mais delta (padrão 1). O primeiro play coloca a citação no ar no passo 0; um segundo play passa do único passo, então executa o stop e retorna currentStep: undefined. Cada ação incrementa this._rev, e um stop só remove .visible se nada mais novo tiver começado — play → stop → play sem esperar termina com a citação 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(1300);
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 };
}{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "dev.ograf.tutorial.quote",
"version": "1.0.0",
"name": "Full Page Quote",
"description": "Cinematic full-screen quote card with staggered reveal of text, divider, and attribution. 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": {
"text": {
"type": "string",
"title": "Quote",
"gddType": "multi-line",
"default": "Open graphics, open broadcast, open standards. That's the future."
},
"author": {
"type": "string",
"title": "Author",
"gddType": "single-line",
"default": "Demo Quote"
},
"role": {
"type": "string",
"title": "Role",
"gddType": "single-line",
"default": "Sample Credit"
}
}
}
}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.
quote.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
Você aprendeu transições CSS escalonadas, layouts em tela cheia, contraste tipográfico e como coordenar o tempo entre CSS e JavaScript.

Lower third
Nome e cargo sobre a imagem

Bug / AO VIVO
Indicador de canto com pulso

Ticker de notícias
Manchetes rolando na tela

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