Cargando...
Cargando...
Una elegante tarjeta de cita a pantalla completa — como las que se usan para frases de entrevistas, secciones motivacionales o entradillas editoriales. El texto, el separador y la atribución aparecen en una secuencia escalonada con impacto cinematográfico.
La magia está en los retardos de las transiciones CSS. El texto y la atribución empiezan transparentes y un poco por debajo de su posición final; el separador empieza con ancho cero. Al añadir la clase .visible, se animan en secuencia:
delay: 0.3sTexto de la cita
Aparece subiendo desde abajo
delay: 0.4sLínea separadora
Crece desde el centro
delay: 0.5sAtribución
Aparece subiendo al final
Todo vive dentro de .quote-root. El reset se limita con :where(.quote-root, …), así que nunca cambia los estilos de la página del renderizador, y la raíz ocupa la caja que proporcione el renderizador con position: absolute; inset: 0. El estado oculto de cada hijo (como .quote-text a continuación) está 20px más abajo y con opacidad cero; las reglas de .visible llevan los retardos.
/* 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);
}Consejo de diseño
El fondo parte de scale(1.1) y hace la transición a scale(1). Así se crea un sutil efecto de "cámara que se asienta" — el fondo se ajusta suavemente mientras aparece la cita. Aire cinematográfico con una sola línea de CSS.
Esta plantilla usa dos fuentes para crear contraste:
"La cita"
Instrument Serif — cursiva, grande (48px). La voz editorial y elegante.
La atribución
CARGO / TÍTULO
Inter — sans-serif limpia y moderna. La voz informativa.
Como la animación escalonada dura más que un simple deslizamiento, la promise de playAction() espera 1300ms antes de resolverse — lo que dura la aparición más lenta. Para entonces el fondo (1s), el texto de la cita (0.3s de retardo + 0.8s), el separador (0.4s + 0.6s) y la atribución (0.5s + 0.8s) ya están en su sitio. stopAction() desvanece la tarjeta entera en 500ms.
El resto sigue el modelo de pasos de OGraf. resolveTargetStep() toma goto si se indica; si no, el paso actual más delta (1 por defecto). El primer play pone la cita en antena en el paso 0; un segundo play se pasa del único paso, así que ejecuta el stop y devuelve currentStep: undefined. Cada acción incrementa this._rev, y un stop solo quita .visible si no ha empezado nada más reciente — play → stop → play sin esperar termina en antena.
/**
* 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"
}
}
}
}Un paquete OGraf Graphics Definition v1 real. Un renderizador compatible lee el manifiesto y gestiona el ciclo de vida. Con licencia MIT; úsalo en cualquier sistema compatible con OGraf.
quote.ograf.json
Manifiesto — lo que lee el renderizador (id, schema, flags del ciclo de vida)
graphic.mjs
Web Component con load / play / update / stop / customAction / dispose
style.css
Hoja de estilos, cargada por graphic.mjs mediante una etiqueta <link>
thumbnail.webp
Vista previa de 1920×1080, declarada en el manifiesto
README.md
Notas de uso
LICENSE
MIT
Despliégalo en un renderizador OGraf compatible
Has aprendido transiciones CSS escalonadas, composiciones a pantalla completa, contraste tipográfico y cómo coordinar los tiempos entre CSS y JavaScript.

Rótulo inferior
Nombre y cargo sobreimpresos

Mosca / EN DIRECTO
Indicador de esquina con pulso

Ticker de noticias
Titulares en desplazamiento

Barras electorales
Gráfico de porcentajes animado

Alineación deportiva
Cuadrícula con la plantilla del equipo

Marcador
Marcador de partido en directo

Cuenta atrás
Reloj que avanza solo

Última hora
Alerta urgente a pantalla completa

Previsión meteorológica
Condiciones actuales y previsión a 3 días

Tarjeta de redes sociales
Publicación sobreimpresa con avatar