Cargando...
Cargando...
Una mosca es un pequeño elemento de marca — indicador EN DIRECTO, logo del canal, distintivo de un evento — que se coloca en una esquina de la pantalla. Aparece con una animación de escala y se desvanece limpiamente.
Posición
Esquina superior derecha. position: absolute; top: 40px; right: 40px — nunca fixed, que se escaparía al viewport en lugar de quedarse en el área de render de 1920×1080.
Animación
Escala desde el 50% + desenfoque en lugar de deslizamiento. Un efecto de "materialización" — menos intrusivo que un deslizamiento para algo que se queda en pantalla.
Punto EN DIRECTO pulsante
Un pulso con @keyframes de CSS sobre un elemento hermano con posición absoluta crea el aviso EN DIRECTO al estilo broadcast sin nada de 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"
}
}
}
}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.
bug.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
En lugar de entrar deslizándose, la mosca crece desde el 50% con un desenfoque de 8px. El estado de reposo vive en .bug; el JavaScript solo alterna las clases visible y out. Así se consigue un sutil efecto de "materialización", menos intrusivo que un deslizamiento — perfecto para algo que se queda en la esquina.
.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;
}Consejo de diseño
La animación de entrada es un ease-out de 0.6s que se asienta con suavidad; la de salida (.bug.out) solo encoge hasta el 80% (no el 50%) en un ease-in-out más corto, de 0.4s, y la opacidad y el desenfoque desaparecen en 0.3s. Esta asimetría — entrada suave, salida rápida — resulta natural. El ojo se fija en la entrada, pero apenas registra la salida.
La misma estructura que el rótulo inferior: un <link> a la hoja de estilos (URL absoluta mediante import.meta.url), un _initDom() diferido y los seis métodos del ciclo de vida. Sin customElements.define() a nivel de módulo — la etiqueta la elige el renderizador. Este es el archivo completo de la descarga:
/**
* 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() sigue la especificación: goto si se indica; si no, el paso actual (-1 antes del primer play) más delta (1 por defecto). La mosca tiene un solo paso, así que el primer play la pone en antena en el paso 0 y un segundo play la saca de antena y devuelve currentStep: undefined.this._rev. stopAction() solo quita .visible tras sus 400ms si no ha empezado ninguna acción más reciente, así que play → stop → play enviados sin esperar terminan en antena.load() y updateAction() aplican cada campo que sea !== undefined: envía solo sublabel para cambiar únicamente ese campo, o una cadena vacía para borrarlo.customAction({ id, payload, skipAnimation }) con un id tomado de customActions en el manifiesto. La mosca no declara ninguna, así que cualquier id recibe { statusCode: 404, statusMessage } — 4xx es el rango de error de la especificación.El mismo patrón de paquete OGraf — manifiesto, CSS, Web Component. Otro aspecto, la misma interoperabilidad.

Rótulo inferior
Nombre y cargo sobreimpresos

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

Tarjeta de redes sociales
Publicación sobreimpresa con avatar