Skip to content

Instantly share code, notes, and snippets.

@ChristopherBiscardi
Created April 25, 2026 02:24
Show Gist options
  • Select an option

  • Save ChristopherBiscardi/f1c951fc3ebe7f12c5f28f54a2b374fc to your computer and use it in GitHub Desktop.

Select an option

Save ChristopherBiscardi/f1c951fc3ebe7f12c5f28f54a2b374fc to your computer and use it in GitHub Desktop.
Davinci ograf tests
const DEFAULT_STATE = {
liveBadgeText: "LIVE",
subtitle: "LOREM IPSUM DOLOR SIT AMET",
breakingText: "BREAKING",
newsText: "NEWS",
categoryLabel: "LOREM IPSUM",
headline: "HEADLINE HERE",
tickerText:
"LOREM IPSUM DOLOR SIT AMET, CONSECTETUER ADIPISCING ELIT, SED DIAM NONUMMY NIBH EUISMOD TINCIDUNT",
liveBadgeColor: "#c0392b",
subtitleBarColor: "#1a1a1a",
breakingBgColor: "#1a2744",
newsBgColor: "#f0c030",
categoryColor: "#2563eb",
headlineColor: "#c0392b",
tickerBgColor: "#f5f3ee",
};
function clamp(v, min, max) {
return Math.max(min, Math.min(v, max));
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
const STYLE_TEXT = `
:host {
position: absolute;
inset: 0;
display: block;
pointer-events: none;
font-family: "Arial", "Helvetica Neue", sans-serif;
--live-badge-color: #c0392b;
--subtitle-bar-color: #1a1a1a;
--breaking-bg-color: #1a2744;
--news-bg-color: #f0c030;
--category-color: #2563eb;
--headline-color: #c0392b;
--ticker-bg-color: #f5f3ee;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
.scene {
position: absolute;
inset: 0;
opacity: 0;
will-change: opacity;
}
/* ── Top-Left Logo Bug ── */
.logo-bug {
position: absolute;
top: 50px;
left: 60px;
display: flex;
flex-direction: column;
gap: 0;
}
.logo-top-row {
display: flex;
align-items: stretch;
height: 30px;
}
.live-badge {
background: var(--live-badge-color);
color: #fff;
font-weight: 800;
font-style: italic;
font-size: 18px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.subtitle-bar {
background: var(--subtitle-bar-color);
color: #fff;
font-weight: 600;
font-size: 14px;
padding: 0 16px;
display: flex;
align-items: center;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
.logo-bottom-row {
display: flex;
align-items: stretch;
height: 52px;
}
.breaking-text {
background: var(--breaking-bg-color);
color: #fff;
font-weight: 900;
font-size: 38px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.02em;
line-height: 1;
}
.news-text {
background: var(--news-bg-color);
color: #1a1a1a;
font-weight: 900;
font-size: 38px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.02em;
line-height: 1;
border: 2px solid #333;
border-left: none;
}
/* ── Bottom Lower Third ── */
.lower-third {
position: absolute;
bottom: 80px;
left: 0;
right: 60px;
will-change: transform, opacity;
}
.side-panel {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 55px;
background: #000;
will-change: transform;
}
.lower-content {
margin-left: 55px;
display: flex;
flex-direction: column;
}
.category-row {
display: flex;
align-items: stretch;
will-change: opacity, transform;
}
.category-label {
background: var(--category-color);
color: #fff;
font-weight: 800;
font-size: 18px;
padding: 6px 24px;
text-transform: uppercase;
letter-spacing: 0.06em;
display: flex;
align-items: center;
white-space: nowrap;
}
.headline-row {
display: flex;
align-items: stretch;
will-change: transform;
}
.headline-bar {
background: var(--headline-color);
color: #fff;
font-weight: 800;
font-style: italic;
font-size: 52px;
padding: 10px 30px;
display: flex;
align-items: center;
white-space: nowrap;
letter-spacing: 0.01em;
line-height: 1.1;
}
.ticker-row {
position: relative;
height: 40px;
background: var(--ticker-bg-color);
overflow: hidden;
will-change: transform;
transform-origin: left center;
border-bottom: 2px solid rgba(0,0,0,0.08);
}
.ticker-text {
position: absolute;
top: 0;
left: 0;
height: 100%;
display: flex;
align-items: center;
white-space: nowrap;
font-weight: 700;
font-style: italic;
font-size: 20px;
color: #1a1a1a;
letter-spacing: 0.03em;
text-transform: uppercase;
will-change: transform;
padding-right: 100px;
}
`;
class BreakingNewsV2Graphic extends HTMLElement {
constructor() {
super();
this._state = { ...DEFAULT_STATE };
this._isVisible = false;
this._currentStep = undefined;
this._animations = [];
this._tickerWidth = 0;
const root = this.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = STYLE_TEXT;
// Scene
const scene = document.createElement("div");
scene.className = "scene";
// ── Logo Bug ──
const logoBug = document.createElement("div");
logoBug.className = "logo-bug";
const logoTopRow = document.createElement("div");
logoTopRow.className = "logo-top-row";
const liveBadge = document.createElement("div");
liveBadge.className = "live-badge";
const subtitleBar = document.createElement("div");
subtitleBar.className = "subtitle-bar";
logoTopRow.append(liveBadge, subtitleBar);
const logoBottomRow = document.createElement("div");
logoBottomRow.className = "logo-bottom-row";
const breakingTextEl = document.createElement("div");
breakingTextEl.className = "breaking-text";
const newsTextEl = document.createElement("div");
newsTextEl.className = "news-text";
logoBottomRow.append(breakingTextEl, newsTextEl);
logoBug.append(logoTopRow, logoBottomRow);
// ── Lower Third ──
const lowerThird = document.createElement("div");
lowerThird.className = "lower-third";
const sidePanel = document.createElement("div");
sidePanel.className = "side-panel";
const lowerContent = document.createElement("div");
lowerContent.className = "lower-content";
const categoryRow = document.createElement("div");
categoryRow.className = "category-row";
const categoryLabel = document.createElement("div");
categoryLabel.className = "category-label";
categoryRow.append(categoryLabel);
const headlineRow = document.createElement("div");
headlineRow.className = "headline-row";
const headlineBar = document.createElement("div");
headlineBar.className = "headline-bar";
headlineRow.append(headlineBar);
const tickerRow = document.createElement("div");
tickerRow.className = "ticker-row";
const tickerText = document.createElement("div");
tickerText.className = "ticker-text";
tickerRow.append(tickerText);
lowerContent.append(categoryRow, headlineRow, tickerRow);
lowerThird.append(sidePanel, lowerContent);
scene.append(logoBug, lowerThird);
root.append(style, scene);
this._elements = {
scene,
logoBug,
liveBadge,
subtitleBar,
breakingTextEl,
newsTextEl,
sidePanel,
lowerThird,
categoryRow,
categoryLabel,
headlineRow,
headlineBar,
tickerRow,
tickerText,
};
}
connectedCallback() {}
async load(params) {
this._state = { ...DEFAULT_STATE, ...(params.data || {}) };
this._applyState();
this._setFrame(0);
return { statusCode: 200 };
}
async dispose() {
this._cancelAnimations();
this._elements.scene.remove();
return { statusCode: 200 };
}
async playAction(params) {
this._setFrame(0.5);
this._currentStep = 1;
return { statusCode: 200, currentStep: this._currentStep };
}
async stopAction(params) {
this._setFrame(-1);
this._currentStep = undefined;
return { statusCode: 200 };
}
async updateAction(params) {
this._state = { ...this._state, ...(params.data || {}) };
this._applyState();
return { statusCode: 200 };
}
async customAction() {
return { statusCode: 200 };
}
async setActionsSchedule(schedule) {
return { statusCode: 200 };
}
async goToTime(time) {
const timestamp = time?.timestamp ?? 0;
this._setFrame(timestamp / 1000);
return { statusCode: 200 };
}
_applyState() {
const s = this._state;
// Text fields
this._elements.liveBadge.textContent = s.liveBadgeText;
this._elements.subtitleBar.textContent = s.subtitle;
this._elements.breakingTextEl.textContent = s.breakingText;
this._elements.newsTextEl.textContent = s.newsText;
this._elements.categoryLabel.textContent = s.categoryLabel;
this._elements.headlineBar.textContent = s.headline;
this._elements.tickerText.textContent = s.tickerText;
// Color fields
if (s.liveBadgeColor) this.style.setProperty("--live-badge-color", s.liveBadgeColor);
if (s.subtitleBarColor) this.style.setProperty("--subtitle-bar-color", s.subtitleBarColor);
if (s.breakingBgColor) this.style.setProperty("--breaking-bg-color", s.breakingBgColor);
if (s.newsBgColor) this.style.setProperty("--news-bg-color", s.newsBgColor);
if (s.categoryColor) this.style.setProperty("--category-color", s.categoryColor);
if (s.headlineColor) this.style.setProperty("--headline-color", s.headlineColor);
if (s.tickerBgColor) this.style.setProperty("--ticker-bg-color", s.tickerBgColor);
// Reset ticker width measurement for scrolling calculation
this._tickerWidth = 0;
}
_cancelAnimations() {
this._animations.forEach((a) => a.cancel());
this._animations = [];
}
_measureTickerWidth() {
if (this._tickerWidth <= 0) {
const el = this._elements.tickerText;
this._tickerWidth = el.scrollWidth || el.offsetWidth || 1600;
}
return this._tickerWidth;
}
_setFrame(seconds) {
const {
scene,
logoBug,
sidePanel,
lowerThird,
categoryRow,
headlineRow,
tickerRow,
tickerText,
} = this._elements;
const duration = 11;
// ── Hidden state ──
if (seconds < 0 || seconds > duration) {
scene.style.opacity = "0";
sidePanel.style.transform = "translateX(-100%)";
headlineRow.style.transform = "translateX(-110%)";
categoryRow.style.opacity = "0";
categoryRow.style.transform = "translateY(10px)";
tickerRow.style.transform = "scaleX(0)";
tickerText.style.transform = "translateX(100%)";
this._isVisible = false;
return;
}
scene.style.opacity = "1";
this._isVisible = true;
// Logo bug is always visible when scene is active
logoBug.style.opacity = "1";
// ── Phase 1: Side panel + Headline slide in (0 - 0.3s) ──
const slideProgress = easeOutCubic(clamp(seconds / 0.3, 0, 1));
sidePanel.style.transform = `translateX(${(-100 + 100 * slideProgress).toFixed(1)}%)`;
headlineRow.style.transform = `translateX(${(-110 + 110 * slideProgress).toFixed(1)}%)`;
// ── Phase 2: Category label fade in (0.3 - 0.7s) ──
const catProgress = easeOutCubic(clamp((seconds - 0.3) / 0.4, 0, 1));
categoryRow.style.opacity = String(catProgress);
categoryRow.style.transform = `translateY(${(10 - 10 * catProgress).toFixed(1)}px)`;
// ── Phase 3: Ticker bar expand (0.7 - 1.2s) ──
const tickerExpandProgress = easeOutCubic(clamp((seconds - 0.7) / 0.5, 0, 1));
tickerRow.style.transform = `scaleX(${tickerExpandProgress.toFixed(4)})`;
// ── Phase 4: Ticker text scroll (1.2s onward) ──
if (seconds >= 1.2) {
const tickerW = this._measureTickerWidth();
const containerW = 1920 - 55 - 60; // full width minus side panel minus right margin
const scrollElapsed = seconds - 1.2;
const scrollSpeed = 120; // pixels per second
const totalScrollDist = tickerW + containerW;
// Start from right edge, scroll left
const offset = containerW - scrollElapsed * scrollSpeed;
// Loop the scroll
const loopedOffset =
((offset % totalScrollDist) + totalScrollDist) % totalScrollDist -
tickerW;
tickerText.style.transform = `translateX(${loopedOffset.toFixed(1)}px)`;
} else {
// Before ticker starts, hide text off to the right
tickerText.style.transform = `translateX(${1920}px)`;
}
}
}
export default BreakingNewsV2Graphic;
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "modern-breaking-news-v2",
"version": "1.0.0",
"name": "Breaking News Broadcast",
"description": "Professional broadcast-style breaking news lower-third with logo bug and scrolling news ticker.",
"author": {
"name": "OGraf Templates"
},
"main": "Breaking-News.js",
"duration": 11,
"supportsRealTime": false,
"supportsNonRealTime": true,
"stepCount": 1,
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"liveBadgeText": {
"type": "string",
"title": "Live Badge Text",
"default": "LIVE"
},
"subtitle": {
"type": "string",
"title": "Subtitle",
"default": "LOREM IPSUM DOLOR SIT AMET"
},
"breakingText": {
"type": "string",
"title": "Breaking Text",
"default": "BREAKING"
},
"newsText": {
"type": "string",
"title": "News Text",
"default": "NEWS"
},
"categoryLabel": {
"type": "string",
"title": "Category Label",
"default": "LOREM IPSUM"
},
"headline": {
"type": "string",
"title": "Headline",
"default": "HEADLINE HERE"
},
"tickerText": {
"type": "string",
"title": "Ticker Text",
"default": "LOREM IPSUM DOLOR SIT AMET, CONSECTETUER ADIPISCING ELIT, SED DIAM NONUMMY NIBH EUISMOD TINCIDUNT"
},
"liveBadgeColor": {
"title": "Live Badge Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#c0392b"
},
"subtitleBarColor": {
"title": "Subtitle Bar Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#1a1a1a"
},
"breakingBgColor": {
"title": "Breaking Background Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#1a2744"
},
"newsBgColor": {
"title": "News Background Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#f0c030"
},
"categoryColor": {
"title": "Category Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#2563eb"
},
"headlineColor": {
"title": "Headline Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#c0392b"
},
"tickerBgColor": {
"title": "Ticker Background Color",
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"gddType": "color-rrggbb",
"default": "#f5f3ee"
}
},
"required": [
"headline"
]
},
"renderRequirements": [
{
"resolution": {
"width": {
"ideal": 1920
},
"height": {
"ideal": 1080
}
},
"frameRate": {
"ideal": 30
}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment