A compressed Developer's Guide
About: LTN Schedule (version 10.2+) provides a HTML graphics overlay template engine that allows you to create professional, animated graphics for live production. This guide covers the essential components that new template developers need to understand when building SPX-compatible templates for use with LTN Schedule.
Whether you're creating custom graphics from scratch or adapting existing HTML templates, understanding these core concepts is crucial for successful template development.
Flexible HTML Template Structure
In just a few steps you can use and control actually every html based graphical overlay - that works in a browser - also within LTN Schedule as a graphical overlay. The key to that is that you deliver your template to LTN Schedule as one .zip file. The file and folder structure within this zip file, should match your given references of scripts, css or javascript from the root html-file as relative folder structure - like this:
or
When you are ready with your template creation (or for testing), just zip your folder with all the necessary files and folders, that are needed for you template, and upload this .zip file to LTN Schedule - as described in this article here.
or
Understanding the SPXGCTemplateDefinition Object
What Is It?
SPXGCTemplateDefinition is a JavaScript object that acts as the bridge between your HTML template and LTN Schedule's control interface. It defines:- How the template should be played out (which server, channel, layer)
- What input fields operators will see in the UI
- Default values for those fields
- Animation behavior and timing
Where Does It Go?
<script> tag within the <head> section of your HTML file, typically as the last item before the closing </head> tag.Basic Structure
<script>
window.SPXGCTemplateDefinition = {
"description": "My Template Name",
"webplayout": "7",
"out": "manual",
"uicolor": "2",
"DataFields": [
// Field definitions go here
]
};
</script>Key Properties Explained
| Property | Purpose | Example |
| description | Human-readable name shown in LTN Schedule | Ticker with Background |
| webplayout | Preview Layer of browser playback (1-20, where 20 is top) | 7 |
| out | How template exits: `manual` (press STOP), `none` (no exit animation), or milliseconds | manual |
| DataFields | Array of input fields for operators | See below |
DataFields: Defining User Input
DataFields array defines what input controls operators see in LTN Schedule's interface. Each field has a field name (mapped to a hidden div), a type, and a default value.textfield --> Single-line text inputtextarea --> Multi-line text inputdropdown --> Select from predefined optionsnumber --> Numeric inputcolor --> Color pickercheckbox --> Boolean togglefilelist --> Select files from ASSETS folderhidden --> Variable not editable by user (but used by template)caption --> Static text displayinstruction --> Help textDataFields that need to be part of the SPXGCTemplateDefinition <script>:"DataFields": [
{
"field": "tickerText",
"ftype": "textfield",
"title": "Ticker Text",
"value": "Enter your text here"
},
{
"field": "tickerSpeed",
"ftype": "number",
"title": "Speed (seconds to traverse screen)",
"value": "15"
},
{
"field": "textColor",
"ftype": "color",
"title": "Text Color",
"value": "rgba(0, 0, 0, 1.0)"
},
{
"field": "backgroundColor",
"ftype": "dropdown",
"title": "Background Style",
"value": "light",
"items": [
{ "text": "Light Background", "value": "light" },
{ "text": "Dark Background", "value": "dark" }
]
}
]
Hidden Data Divs: Receiving Data from LTN Schedule
data containers between the control interface and your template logic.<div id="hiddenSpxData" data-info="SPX template fields (hidden)">
<div id="tickerText"></div>
<div id="tickerSpeed"></div>
<div id="textColor"></div>
<div id="backgroundColor"></div>
</div>
Critical Naming Rule
id attribute of each hidden div MUST match the field name in your DataFields definition.- DataField with
"field": "tickerText"→ Hidden div withid="tickerText" -
DataField with
"field": "tickerSpeed"→ Hidden div withid="tickerSpeed"
What Gets Stored
- The system reads the field value from the input control
- It stores the value in the corresponding hidden div's
innerTextproperty -
Your template JavaScript reads from these hidden divs to update the display
Accessing Hidden Data in Your Code
const tickerText = document.getElementById('tickerText').innerText;
const speed = document.getElementById('tickerSpeed').innerText;
const color = document.getElementById('textColor').innerText;Link the spx_interface.js
To execute commands from the interface within LTN Schedule to the SPX template, the functions from the spx_interface.js must be referenced within you html code:
...
<title>My SPX Template</title>
<script src="js/spx_interface.js"></script>
...
In this example, the spx_interface.js is located in the /js folder - see the attached hello_world.zip file to this article with the demo project. You can also find the whole code of the spx_interface.js in the section Complete Template HTML Code below.
The runTemplateUpdate Function
runTemplateUpdate() is called by LTN Schedule whenever:- The template is initially loaded
- An operator updates any field value
- The template needs to refresh
function runTemplateUpdate() {
// 1. Read data from hidden divs
const tickerText = e('tickerText')?.innerText || '';
const speed = e('tickerSpeed')?.innerText || '15';
const color = e('textColor')?.innerText || '#000000';
// 2. Update your template display
document.getElementById('myTextElement').textContent = tickerText;
document.getElementById('myTextElement').style.color = color;
// 3. Recalculate animations if needed
updateAnimation();
// 4. Trigger the animation in
if (typeof runAnimationIN === "function") {
runAnimationIN();
}
}
// Make it available to LTN Schedule
window.runTemplateUpdate = runTemplateUpdate;
Implementation Tips:
- Always check for null/undefined when reading from hidden divs
- Update the DOM with the new values
- Recalculate animations if size, speed, or position changed
- Call
runAnimationIN()at the end to trigger entry animation - Register the function with
window.runTemplateUpdate = runTemplateUpdate
Animation Lifecycle: runAnimationIN and runAnimationOUT
runAnimationIN() --> Called when the graphic should appear/animate inrunAnimationOUT() --> Called when the graphic should disappear/animate out
runAnimationIN: The Entry Animation
- An operator clicks the PLAY button in LTN Schedule
- You want the graphic to enter/appear on screen
function runAnimationIN() {
// Example: Fade in the main element
const mainElement = document.getElementById('mainContent');
mainElement.style.opacity = '0';
mainElement.style.transition = 'opacity 0.5s ease-in';
// Force reflow to apply the 0 opacity
mainElement.offsetHeight;
// Trigger animation
mainElement.style.opacity = '1';
// Or start an animation
startAnimation();
}
window.runAnimationIN = runAnimationIN;
runAnimationOUT: The Exit Animation
- An operator clicks the STOP button in LTN Schedule
-
The template should exit/disappear from screen
function runAnimationOUT() {
// Example: Fade out and hide the element
const mainElement = document.getElementById('mainContent');
mainElement.style.opacity = '0';
mainElement.style.transition = 'opacity 0.5s ease-out';
// Stop any running animations
stopAnimation();
// Hide completely to ensure it doesn't occupy space
setTimeout(() => {
mainElement.style.visibility = 'hidden';
}, 500); // Wait for fade animation to complete
}
window.runAnimationOUT = runAnimationOUT;
Responsive scaling for different browser sizes and resolutions
After using the html-template with LTN Schedule, the html-template (html code) will be displayed locally within the preview area of the graphics tab of LTN Schedule - see number 2 from the screenshot on the using templates page.
When the template is activated, the template will be also used from a browser, running within the LTN Schedule video engine, that actually puts the overlay on top of the video.
The preview window within the LTN Schedule user interface scales in size - dependent on the resolution of the users browser window.
The resolution of the browser, running within the LTN Schedule video engine, depends on the selected output resolution within the transcoding settings. (e.g. 1920x1080 or 1280x720, etc... see LTN Schedule Transcoding Technical Specification).
That´s why it is very important, that the html code of the template is build in a way, that it scales every content, related to the actual browser window resolution, that the proportions of the page (grfx elements and text) always look the same, no matter in what size (browser window size) the page is displayed.
Complete Template HTML Code
HTML Code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My SPX Template</title>
<script src="js/spx_interface.js"></script>
<style>
:root {
--content-min: 18px;
--content-max: 96px;
}
body {
margin: 0;
padding: 0;
background: transparent;
font-family: Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
box-sizing: border-box;
}
/* Full-viewport holding container (keeps renderer sizing predictable) */
#graphicContainer {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 2vh 4vw;
}
/* Responsive text element
- `clamp()` keeps the font-size between sensible min/max values
- `vw` based value lets text scale with viewport width
- `max-width` and wrapping prevent overflow on narrow viewports */
#content {
opacity: 0;
color: #000;
transition: opacity 0.5s ease;
text-align: center;
display: inline-block;
max-width: 100%;
width: auto;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.05;
padding: 0.1em 0.25em;
font-size: clamp(var(--content-min), 4.5vw, var(--content-max));
}
</style>
</head>
<body>
<div id="graphicContainer">
<div id="content"></div>
</div>
<script>
// ====== TEMPLATE DEFINITION ======
window.SPXGCTemplateDefinition = {
"description": "Hallo World Template",
"playserver": "OVERLAY",
"playchannel": "1",
"playlayer": "7",
"webplayout": "7",
"out": "manual",
"uicolor": "2",
"DataFields": [
{
"field": "titleText",
"ftype": "textfield",
"title": "Title Text",
"value": "Hello World"
},
{
"field": "titleColor",
"ftype": "color",
"title": "Text Color",
"value": "rgba(0, 0, 0, 1.0)"
}
]
};
// ====== TEMPLATE LOGIC ======
function runTemplateUpdate() {
const titleText = document.getElementById('titleText')?.innerText || '';
const titleColor = document.getElementById('titleColor')?.innerText || '#000';
const content = document.getElementById('content');
content.textContent = titleText;
content.style.color = titleColor;
runAnimationIN();
}
function runAnimationIN() {
const content = document.getElementById('content');
content.style.opacity = '1';
}
function runAnimationOUT() {
const content = document.getElementById('content');
content.style.opacity = '0';
}
window.runTemplateUpdate = runTemplateUpdate;
window.runAnimationIN = runAnimationIN;
window.runAnimationOUT = runAnimationOUT;
</script>
<!-- ====== HIDDEN DATA DIVS ====== -->
<div id="hiddenSpxData" style="display: none;">
<div id="titleText"></div>
<div id="titleColor"></div>
</div>
</body>
</html>
Code of the javascript spx interface file from the open source repository of SPX - spx_interface.js :
// ----------------------------------------------------------------
// (c) Copyright 2021- SPX Graphics (https://spx.graphics)
// ----------------------------------------------------------------
// Receive item data from SPX Graphics Controller
// and store values in hidden DOM elements for
// use in the template.
function update(data) {
var templateData = JSON.parse(data);
console.log('----- Update handler called with data:', templateData)
for (var dataField in templateData) {
var idField = document.getElementById(dataField);
if (idField) {
let fString = templateData[dataField];
if ( fString != 'undefined' && fString != 'null' ) {
idField.innerText = fString
} else {
idField.innerText = '';
}
} else {
switch (dataField) {
case 'comment':
case 'epochID':
// console.warn('FYI: Optional #' + dataField + ' missing from SPX template...');
break;
default:
console.error('ERROR Placeholder #' + dataField + ' missing from SPX template.');
}
}
}
if (typeof runTemplateUpdate === "function") {
runTemplateUpdate() // Play will follow
} else {
console.error('runTemplateUpdate() function missing from SPX template.')
}
}
// Play handler
function play() {
// console.log('----- Play handler called.')
// if (typeof runAnimationIN === "function") {
// runAnimationIN()
// } else {
// console.error('runAnimationIN() function missing from SPX template.')
// }
}
// Stop handler
function stop() {
// console.log('----- Stop handler called.')
if (typeof runAnimationOUT === "function") {
runAnimationOUT()
} else {
console.error('runAnimationOUT() function missing from SPX template.')
}
}
// Continue handler
function next(data) {
console.log('----- Next handler called.')
if (typeof runAnimationNEXT === "function") {
runAnimationNEXT()
} else {
console.error('runAnimationNEXT() function missing from SPX template.')
}
}
// Encoded text to HTML
function htmlDecode(txt) {
var doc = new DOMParser().parseFromString(txt, "text/html");
return doc.documentElement.textContent;
}
// Utility function
function e(elementID) {
if (!elementID) {
console.warn('Element ID is falsy, returning null.');
return null;
}
if (!document.getElementById(elementID)) {
console.warn('Element ' + elementID + ' not found, returning null.');
return null;
}
return document.getElementById(elementID);
}
window.onerror = function (msg, url, row, col, error) {
let err = {};
err.file = url;
err.message = msg;
err.line = row;
console.log('%c' + 'SPX Template Error Detected:', 'font-weight:bold; font-size: 1.2em; margin-top: 2em;');
console.table(err);
// spxlog('Template Error Auto Detected: file: ' + url + ', line: ' + row + ', msg; ' + msg,'WARN')
};
function validString(str) {
let S = str.toUpperCase();
// console.log('checking validString(' + S +');');
switch (S) {
case "UNDEFINED":
case "NULL":
case "":
return false // not a valid string
break;
}
return true; // is a valid string
}
You can find the attached hello_world.zip template file - ready to upload to LTN Schedule - here.
Best Practices for New Developers
- Always define a complete
SPXGCTemplateDefinition- LTN Schedule requires this to recognize your template - Match hidden div IDs to DataField names - This is critical for data flow
- Test in LTN Schedule - Don't rely only on browser preview
- Use descriptive field titles - Help operators understand what each field controls
- Provide sensible default values - Operators should see a usable template before editing
- Register functions globally - Always do
window.runAnimationIN = runAnimationIN - Handle null/undefined data - Use optional chaining or fallbacks
- Hide everything on exit - Use both
opacity: 0andvisibility: hidden
Don'ts ❌
- Don't forget to hide the template on runAnimationOUT - Invisible shouldn't mean it's still there
- Don't use IDs that don't match DataField names - Data won't reach your template
- Don't assume data will always be present - Always check and provide defaults
- Don't use word processors - Always use plain text editors (VS Code, Sublime, etc.)
- Don't forget the SPXGCTemplateDefinition - Templates won't import without it
Resources and Further Learning
SPXGCTemplateDefinition- Tells LTN Schedule how to integrate your template- Hidden Divs - Receive operator input from the UI
runTemplateUpdate()- Reads data and updates the displayrunAnimationIN/OUT()- Control when the graphic appears and disappears
Happy templating! 🎬