Cargando...
Cargando...
Los informativos muestran a menudo publicaciones de redes sociales en pantalla — un tuit de una figura pública, un comunicado oficial o una publicación viral. Esta tarjeta muestra el nombre del usuario, su @ y el texto de la publicación, con un avatar generado automáticamente. Se coloca en el lado derecho de la pantalla, de modo que el presentador sigue viéndose a la izquierda.
Posición a la derecha
Usa right: 48px; bottom: 80px en lugar de left. Así el presentador sigue viéndose en el lado izquierdo del encuadre — una convención habitual en televisión para el contenido que se muestra.
Avatar generado automáticamente
No hace falta ninguna imagen. El círculo del avatar muestra las iniciales del usuario, extraídas de su nombre con _getInitials(). Nada que alojar, nada que se pueda romper.
Entrada en dos tiempos
La tarjeta entra deslizándose desde fuera de la pantalla por la derecha con un ligero desenfoque en 0.7s, y el avatar aparece 0.35s después con una curva que se pasa de largo y rebota — todo con CSS, a partir de una única clase .visible.
En lugar de exigir la URL de una foto de perfil (que puede romperse, tener poca resolución o dar problemas de derechos), la tarjeta genera un avatar a partir del nombre del usuario. _getInitials divide el nombre por los espacios, toma la primera letra de cada palabra, la pasa a mayúscula y se queda con las dos primeras: "Jane Smith" → "JS", "Dr. Martin King" → "DM", "Madonna" → "M", y un nombre vacío → un avatar vacío. _applyData lo comparten load() y updateAction(); aplica cada campo que sea !== undefined, así que una actualización parcial solo toca los campos que envías y una cadena vacía borra uno. También acepta una cadena opcional platform para la etiqueta oscura de la cabecera. El schema del manifiesto no la declara, así que por defecto la etiqueta está vacía — y .social-platform:empty la oculta en lugar de dejar una cápsula en blanco.
_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 };
}La reproducción y la parada siguen el modelo de pasos de OGraf. El primer play pone la tarjeta en antena en el paso 0 y se resuelve a los 700ms; un segundo play se pasa del único paso, ejecuta el stop y devuelve currentStep: undefined. Cada acción incrementa this._rev, y el stop solo quita .visible tras sus 500ms si no ha empezado nada más reciente — así que play → stop → play enviados sin esperar terminan 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(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 };
}La tarjeta entra deslizándose desde el borde derecho: .social parte de translateX(120%), transparente y desenfocada, y .social.out la devuelve a su sitio en 0.5s. Una barra con degradado de 4px dibujada con ::before da a la tarjeta blanca su acento azul a la izquierda, y el avatar es un círculo azul sólido que crece desde el 40%. El reset se limita con :where(.social-card-root, …), así que nunca cambia los estilos de la página del 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;
}Consejo de diseño
El círculo del avatar usa la primera letra de cada palabra del nombre del usuario — "Jane Smith" se convierte en "JS". Así no hace falta ningún recurso de imagen externo. El relleno sólido en el azul de la marca y la aparición retardada hacen que el círculo parezca intencionado, y no un sustituto de una imagen que falta.
{
"$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."
}
}
}
}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.
social-card.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
Posición a la derecha, avatar con iniciales generado automáticamente y un acento azul limpio — lista para mostrar publicaciones de redes sociales en antena.

Rótulo inferior
Nombre y cargo sobreimpresos

Mosca / EN DIRECTO
Indicador de esquina con pulso

Ticker de noticias
Titulares en desplazamiento

Cita a pantalla completa
Tipografía cinematográfica a pantalla completa

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