Developers
Split Control's graphics are plain web files: HTML, CSS and JavaScript modules following the EBU OGraf standard. A look is a family of graphics sharing one style. This page shows how to make your own and test it.
Preview tool: try every look with a simulated race, or load your own look's zip, right in your browser.
Start here
You can try the graphics in your browser before installing anything. For a simple colour change, start with an existing look. To make a new graphic, you need a text editor and basic HTML, CSS and JavaScript; the shared code handles playback for you.
| I want to… | Start with | What you need |
|---|---|---|
| Change colours or the event logo | Plan → Theme in the app | No coding |
| Change fonts, spacing or animations | Change an existing look | CSS and a downloaded look zip |
| Add a new graphic with my own text | Your first graphic | Three small code changes, shown below |
| Show a class’s runners or results | Inputs and feeds | A desk declaration and a timing datasource file |
| Test or share my work | Test and Share | The browser preview, then the app |
The words used in this guide
| Word | Plain meaning | Example |
|---|---|---|
| Look | A folder of graphics with the same visual style. | standard or sof |
| Graphic | One thing you put on screen. | A start list or a name plate |
| Manifest | A JSON file describing a graphic and its controls. | standard-results.ograf.json |
| Kind | The drawing/type of graphic. CSS uses it in a class name. | results → .gfx--results |
| Desk key | The stable identifier used by favourites, command numbers and remote controls. | timer uses the clock drawing |
| Schema | The list of editable fields, their types and defaults. | title is a text field |
| View | A JavaScript function that turns data into HTML. | Returns { html, steps } |
| Feed | A type of data the app can fill in. | timing or weather |
| Step | One press of Next. It may change the page or focus a runner. | SOFT Runner presentations |
Commands in a terminal run from the repository folder. Code blocks labelled with a filename go in that file. Keep JSON keys in double quotes, and leave out trailing commas.
Preview tool
The preview runs the real graphics, the same files the app uses, without installing anything:
- Look and graphic: every look on this site, plus any look you load as a zip. Your zip stays in your browser; nothing is uploaded.
- Show, Update, pages, Hide as on the Live desk, with the same keys: Space, U, ← →, H.
- Race simulator: twelve runners start at a two-minute interval and pass four radio controls and the finish, with one mispunch and one runner who gives up. Run it at 1× to 60×, or jump two minutes ahead. With Live updates on, graphics on air change as results come in, as they do in the app.
- Data: timing point, runner, broadcast language, clubs or flags, the three theme colours, and any extra fields as JSON (titles, texts, names).
- Backgrounds: forest, transparent, dark and light, to check contrast.
Load the same zip again after a change: it replaces the earlier copy. Remove loaded looks clears them all.
How a look works
The app provides 17 standard drawings through sets/_core/. A look can use
these drawings, leave graphics out, or add a custom drawing with its own Live desk
declaration. Runner presentations and Finish watch use shared views with per-look styling; IOF and ČO omit them. Shared style and motion live in
the look’s CSS.
sets/
├── _core/ shared code (ships with the app, not part of a look)
│ ├── graphic.js the OGraf Web Component: lifecycle, pages, clocks, scaling
│ ├── views.js data → markup for standard graphics
│ └── base.css placement and structure shared by all looks
└── my-look/ one look
├── set.json name, description, the three theme colours, approvedEventsOnly
├── set.js durations, name styles, labels (read by the graphics)
├── theme.css the style and the motion
├── fonts/ fonts.css, font files and licence (see Fonts)
└── my-look-results/ one folder per graphic: manifest + a 4-line graphic.js
├── my-look-results.ograf.json
└── graphic.js
A graphic's graphic.js is only this:
import { createGraphic } from '../../_core/graphic.js';
import set from '../set.js';
export default createGraphic('results', set);
Because graphics import ../../_core/, a look always runs with the shared code of
the app it's in. That keeps every look up to date with new features and fixes.
Change an existing look
For new colours, fonts or details on a look you already use, no tools are needed (for a new font, see Fonts):
- In Split Control, Graphics library → Export… next to the look, or download it from the Looks page.
- Unzip it and edit
theme.css(andset.jsonfor the name and colours). Keep the look's id ("id"inset.json). - Zip the look's folder again and load it in the preview to check it.
- Import it in the app: it replaces the look with the same id. For a built-in look like Standard, removing your copy brings the original back.
Make a new look
A new look needs its own id, and its graphics need unique ids. The repository has a generator that writes all 17 graphic folders for you.
- Get the code:
git clone https://github.com/opensourceorienteering/O-TV-Graphics, thennpm install(Node.js 22 or newer). - Copy a look that is close to what you want, e.g.
templates/sets/standardtotemplates/sets/my-look, and delete its graphic folders (standard-*). - Edit
set.json(id, name, description, colours; add"approvedEventsOnly": trueif the look copies an organisation's official graphics, so it is marked "Only use at approved events") andset.js(id, andcolorVars: the CSS variables the three theme colours set). - Add the look to
SETSinscripts/generate-template-sets.jsand runnpm run generate:sets. - Style it in
theme.css. To see changes, either run the app (npm run dev, then Scan library), or zip the folder and load it in the preview.
Every look in the repository is a working example. standard is a good start;
sof follows a full graphic manual. See also
templates/sets/README.md.
Your first graphic: a welcome card
This example adds a small, editable welcome card to your local Standard look. It needs no competition, runners or internet data. Use a working copy of the repository; editing this copy does not change anyone else’s installed app.
1. Install and start the app
Install Node.js 22 or newer. Open a terminal, clone the repository and install its dependencies. Native NDI dependencies may need the platform build tools listed in the repository README.
git clone https://github.com/opensourceorienteering/O-TV-Graphics.git
cd O-TV-Graphics
npm install
npm run dev
If your shell has Electron’s Node mode enabled, start with
env -u ELECTRON_RUN_AS_NODE npm run dev on macOS/Linux. Split Control’s
dev server uses port 5175; another project can use 5173 at the same time.
2. Create the manifest
Create the folder templates/sets/standard/standard-welcome/. Inside it,
create standard-welcome.ograf.json with this complete JSON:
{
"$schema": "https://ograf.ebu.io/v1/specification/json-schemas/graphics/schema.json",
"id": "standard-welcome",
"name": "Standard – Welcome card",
"main": "graphic.js",
"supportsRealTime": true,
"supportsNonRealTime": false,
"stepCount": 1,
"schema": {
"type": "object",
"properties": {
"title": { "type": "string", "title": "Heading", "default": "Welcome" },
"message": { "type": "string", "title": "Message", "default": "Good luck to every runner!" }
}
},
"v_olgrafik_set": "standard",
"v_olgrafik_kind": "welcome-card",
"v_olgrafik_zone": "main",
"v_olgrafik_desk": {
"label": "Welcome card",
"short": "Welcome",
"inputs": [],
"required": [],
"fields": ["title", "message"],
"feeds": [],
"behaviours": []
}
}
schema.properties creates the two text fields. fields puts
them in the Live inspector. Empty inputs and feeds mean there
is no class/runner selection and no timing connection to set up.
3. Add the drawing
In the same folder, create graphic.js:
import { createGraphic } from '../../_core/graphic.js';
import { esc } from '../../_core/util.js';
import set from '../set.js';
function welcomeCard(data) {
const title = data.title || 'Welcome';
const message = data.message || 'Good luck to every runner!';
return {
steps: 1,
html: `<div class="panel panel--table">
<header class="hdr"><span class="hdr-title">${esc(title)}</span></header>
<div class="rows"><div class="row">
<span class="c-name">${esc(message)}</span>
</div></div>
</div>`
};
}
export default createGraphic('welcome-card', set, welcomeCard);
esc() safely inserts text into HTML. createGraphic() supplies
Show, Hide, updates and cleanup. The view just describes what to draw.
4. Give the new kind a position
Add this CSS at the end of templates/sets/standard/theme.css:
.gfx--welcome-card .panel {
top: var(--table-top, 144px);
left: var(--table-x, 480px);
right: var(--table-x, 480px);
}
.gfx--welcome-card .row { padding: 16px; }
The picture is designed at 1920 × 1080. These shared table settings give the card the same centred width as Standard’s results and start list. New kinds need a positioning rule; otherwise their panel can sit at the top left.
5. See it on screen
- In Graphics library, scan the library (or restart the dev app).
- Create/open a test event and choose Standard as its look.
- Open Live and choose Welcome card. Type a heading and message.
- Check Preview, then press Show. Edit the message and press Update; press Hide when finished.
Success looks like: a centred card using Standard’s fonts and colours, two editable fields in Live, and changes reaching Program only when you press Update.
6. Try two pages
In the manifest, change stepCount to -1 (the drawing decides
its step count). Replace only the view function with this; keep the imports and export:
function welcomeCard(data, ctx) {
const messages = [data.message || 'Welcome!', 'The next class starts soon.'];
const page = Math.min(Math.max(ctx.step, 1), messages.length);
return {
steps: messages.length,
pageKey: `page-${page}`,
html: `<div class="panel panel--table">
<header class="hdr"><span class="hdr-title">${esc(data.title || 'Welcome')}</span></header>
<div class="rows"><div class="row"><span class="c-name">${esc(messages[page - 1])}</span></div></div>
<footer class="ftr"><span class="ftr-page">${page}/${messages.length}</span></footer>
</div>`
};
}
Rescan/restart, hide the card if it is on air, then Show it again. Use Next/Previous
to move between the two pages. For operator-controlled steps, add
"manualSteps" to the declaration’s behaviours array. For focus
changes on one page, return the same pageKey for those steps.
Add a custom graphic
Add a hand-made OGraf package alongside the standard generated folders. Its manifest
declares v_olgrafik_kind, v_olgrafik_zone and an optional
v_olgrafik_desk with its label, inputs, required inputs, manual fields,
feeds, behaviours and preview data. Known kinds inherit defaults; invalid declarations
fall back and show a warning in Graphics library.
"v_olgrafik_desk": {
"label": "Relay overview",
"inputs": ["class", "page"],
"required": ["class"],
"fields": ["title"],
"feeds": ["timing"],
"behaviours": ["holdOnAir", "titleFromClass"],
"previewData": "results"
}
Declare manual fields in schema.properties and supply a datasource file for
timing mappings. A custom view is passed to createGraphic(kind, set, view).
It returns { html, steps?, pageKey?, key?, rekey? }. Shared lifecycle code
handles pages, clocks and live patches. A new kind also needs an explicit CSS placement
rule using tokens such as --table-top and --table-x.
Use pageKey for focus changes within one visible page and
manualSteps to disable automatic page timers. SOFT’s runner picker uses a
string-array field with v_olgrafik_control: "runner-multi-select" and
v_olgrafik_default: "favourites". Stable desk keys retain command numbers
and favourites when changing looks. Companion reads the current event’s catalogue.
The template developer guide contains the complete contract and examples.
Command and option reference
There are three kinds of “command”: terminal commands for development, methods that the app calls on a graphic, and operator controls in Live. The tables separate them.
Terminal commands · Graphic methods and actions · What a custom view returns · Desk declaration properties · Inputs · Feeds · Behaviours · Existing standard drawings · Live keyboard controls · Common problems
Terminal commands
| Run in the repository folder | What it does | When to use it |
|---|---|---|
npm run dev | Starts the dev app and Vite on port 5175. | While editing a look |
npm run type-check | Checks TypeScript without building. | Before sharing code changes |
npm test -- --run | Runs the unit and backend integration tests once. | After app-side changes |
npm run test:templates | Loads every repository graphic in real Chromium and saves screenshots/report. | After changing markup, CSS or a manifest |
npm run generate:sets | Rewrites the standard generated graphic packages. | After editing the generator; keep custom folders separate |
npx vite build | Builds the app code without making an installer. | To check the production bundle |
npm run build | Type-checks, builds and makes an installer for your platform. | To package the application |
bash scripts/build-site.sh /tmp/split-control-site | Builds the site, previews and look downloads. Replaces the target directory. | To test the website in a dedicated output folder |
bash scripts/zip-looks.sh /tmp/split-control-looks | Creates one downloadable zip per look. | To share updated looks |
Graphic methods and actions
The app calls these methods for you. These examples are useful when writing a test or a custom player; graphic means an already-created graphic element.
| Method/action | Example call | Result |
|---|---|---|
load | await graphic.load({ data: { title: 'Welcome' } }) | Loads styles and prepares the graphic. It is still off air. |
playAction | await graphic.playAction({}) | Shows an off-air graphic; when already visible, advances one step. |
playAction with delta | await graphic.playAction({ delta: -1 }) | Moves back one step when visible. |
playAction with goto | await graphic.playAction({ goto: 1 }) | Goes to a specific one-based step. |
updateAction | await graphic.updateAction({ data: { title: 'Updated' } }) | Replaces the data object and patches the visible content. Send the full data you want to retain. |
stopAction | await graphic.stopAction({}) | Plays the exit and hides the graphic. |
customAction | await graphic.customAction({ id: 'next-page' }) | Built-in alias for the next step. |
customAction | await graphic.customAction({ id: 'previous-page' }) | Built-in alias for the previous step. |
customAction | await graphic.customAction({ id: 'first-page' }) | Built-in alias for step 1. |
dispose | await graphic.dispose() | Stops ticking and removes the graphic’s DOM/listeners. |
Add skipAnimation: true to play, stop or update options when you need an instant state in a test. A “page” action advances a step; a custom graphic can use steps for runner focus instead of page changes.
What a custom view returns
| Property | Example | Meaning |
|---|---|---|
html | html: '<div class="panel">Welcome</div>' | Required HTML string. Escape any operator/timing text with esc(). |
steps | steps: 3 | Total operator steps. Omit for a one-step graphic. |
pageKey | pageKey: 'page-1' | Keep the key the same for in-place focus steps; change it for a page transition. |
rekey + key | rekey: true, key: 'next-start-group' | For content that changes with time. The ticker redraws when the key changes; do not generate a new key on every call. |
Desk declaration properties
| Property | Example | Meaning |
|---|---|---|
key | "welcome-card" | Stable control/favourite ID. Defaults to the kind for new graphics. |
label / short | "Welcome card" / "Welcome" | Full Live label and short palette label. |
inputs | ["class", "page"] | Palette parameters, in the order you want them typed. |
required | ["class"] | Inputs needed to prepare the graphic; each must also be in inputs. |
fields | ["title", "message"] | Schema fields to show in the Live inspector. |
settingsFields | ["nameStyle"] | Schema fields to edit only in template settings. |
feeds | ["timing"] | Existing data sources the graphic uses; timing needs a datasource mapping file too. |
behaviours | ["holdOnAir", "manualSteps"] | Existing desk behaviours; see below. |
rowsPartner | "start-list" | A graphic whose rows-per-page setting this graphic can follow. |
previewData | "results", "splits" or "starts" | What Plan needs to find in real timing data, otherwise it uses samples. Use null to clear an inherited requirement. |
Inputs
| Name | What the operator picks/types | Example |
|---|---|---|
class | A class number or name. | Class 2, or D21 |
runner | A bib or runner name. | Bib 101 |
control | A radio control or finish; start requires startControl. | 45, F; S when enabled |
page | A one-based starting step/page number. | 2 |
Feeds
| Name | Supplies | Setup |
|---|---|---|
timing | Class results, split times and start times. | Timing connection plus a .datasource.json mapping file |
course | Course length, climb, controls and other supported class facts. | A class and a source that supplies course facts; manual values can override |
weather | Condition, temperature, wind and humidity. | Event location/coordinates, or manual weather values |
event | Event title, series, place and date. | Event setup |
Behaviours
| Name | What changes | Requirement |
|---|---|---|
startControl | Allows S (start); no control also means start. | control input |
followNextStarter | Follows the next starter until the operator makes a manual pick. | runner input; the current Live desk has one shared follow state |
freeze | Enables manually stopping a running clock on the way to a control. | runner input; no freeze at the start |
titleFromClass | Uses the class name when the title is empty. | A chosen class |
titleFromLocation | Uses the event location when the title is empty. | Event location |
holdOnAir | Defaults live timing updates to held until Update. | Useful for tables and presentations |
manualSteps | Disables automatic page timers. Only operator actions advance steps. | Useful for presentations |
Existing standard drawings
Pass the kind to createGraphic(kind, set) to reuse its drawing. Desk keys can differ from kinds. Two legacy drawings have no default Live entry; add a desk declaration to offer them there.
| Kind | Graphic | Default desk key |
|---|---|---|
clock | Elapsed timer | timer |
course-info | Course parameters | course-info |
event-title | Race title | event-title |
free-text | Identity plate | free-text |
name-strap | Name strap | No default Live entry |
podium | Podium / flowers | podium |
position-history | Position history | position-history |
results | Results table | results |
runner-identity | Runner identity | runner-identity |
runner-lower-third | Runner lower third | No default Live entry |
runner-splits | Runner split comparison | runner-splits |
runner-start | Runner focus | runner-start |
speaker | Commentators / speaker | speaker |
standings-lower-third | Live split | live-feed |
start-group | Start group | start-group |
start-list | Start list | start-list |
weather | Weather | weather |
All looks except IOF and ČO declare runner-presentations and finish-watch. Its Next action lifts chosen runners one at a time; start-list favourites are selected by default. See the Runner presentations guide. Finish watch uses a prewarning to reveal runners, shows total race time and class placing, and freezes at a measuring point before removing completed rows. See the Finish watch guide.
Live keyboard controls
Use these in Live with focus outside a text field. Command numbers are configurable—check the current desk rather than assuming a fixed number.
| Key | Action |
|---|---|
Space | Show/hide the selected graphic |
U | Update its on-air data from Preview |
H | Hide the active graphic |
← / → | Previous/next step on Program |
T | Freeze/resume an eligible running clock |
Esc | Hide all; in a field, leave the field |
F | Save the draft as a favourite |
Ctrl/Cmd+K | Open the command palette, including from a field |
Palette example: if your Results graphic is number 4, 4 2 F 1 means Results, class 2, finish, first page. 4 F1 recalls that graphic’s first saved favourite. Preparing a command changes Preview; Show puts it on air.
Common problems
| What you see | Check this |
|---|---|
| My graphic is missing from Live. | Check the event’s selected look, unique manifest ID and desk key, and declaration warnings in Graphics library. Rescan after adding a package. |
| The panel is small or at the top left. | Add a placement rule for your exact .gfx--kind; reuse table tokens for a results-sized panel. |
| The inspector has no field. | Declare it in schema.properties and list it in v_olgrafik_desk.fields. |
| The graphic has no timing results. | Check the class, event timing connection, timing feed and .datasource.json mappings. A feed name alone does not fetch a new data type. |
| My data disappears after Update. | updateAction receives a replacement data object. Retain the fields you still need. |
| Next changes the whole page when I only want to focus a runner. | Keep the same pageKey for those steps; use manualSteps to disable timers. |
| The dev server reports a busy port. | Stop the process using 5175 and restart. Other projects may keep using 5173. |
Optional class filters and radio selectors
A timing datasource can set api.classScope: "optional". When the operator
leaves Class empty, mapping combines all known classes and live polling follows them;
each result retains its class. A selected class uses the normal one-class path.
A string schema field with v_olgrafik_control: "radio-control" gets a radio
selector. Add v_olgrafik_includeFinish: true to offer Finish, and
v_olgrafik_distinctFrom: "triggerControl" to require a different point.
Finish watch is the complete example. Stateful custom views receive their graphic
element as this; keep state per element so Preview and Program stay independent.
Styling the graphics
Start theme.css with @import url('../_core/base.css');. The
canvas is 1920 × 1080; graphics scale to the output size.
Markup
.gfx.gfx--<kind> | The graphic, e.g. .gfx--results. Carries data-state (see Motion). |
.panel.panel--<type> | One block: --table, --lower, --strap, --side, --title, --clock, --podium, --chart… |
.hdr / .ftr | Header and footer: .hdr-title, .hdr-sub, .hdr-logo, .ftr-page… |
.rows > .row | Table rows. --i is the row's index, for staggered animation. .row--leader, .row--hl (the runner in focus), .row--status (MP, DNF…). |
| Cells | .c-place, .c-name, .c-id (club, flag), .c-time, .c-bib, .c-start, .c-label, .c-value, .c-change |
Variables
Placement is set with variables, so a look rarely needs to move panels itself:
--margin-x, --margin-y, --table-top,
--table-x, --lower-w, --side-w,
--title-x, --podium-x. Chart lines use --line-0 to
--line-7.
Theme colours
Operators can change three colours per event (Plan → Theme). colorVars in
set.js names the CSS variables they set, and colors in
set.json gives the defaults, in the same order. Derive every other shade from
those three (color-mix(), relative colours), so a change follows everywhere.
Rules every look follows
- Keep the centre of the picture clear: lower thirds bottom left, side panels bottom right, the clock top right.
- Numbers use tabular figures, so running times don't wobble.
- Names and texts end in an ellipsis instead of overflowing. Check long names and accents (Å, Ä, Ö, Ø).
- The event logo appears only on the start list, results and race title card, at least 42 px tall (see The look's own logo).
- Bundle every font the look uses (see Fonts).
The look's own logo
A look can carry a logo of its own, such as a federation's. In Plan, operators choose
Use template logo, Upload custom logo or
No logotype. The graphic tells the look which one with
data-logo on .gfx: template, custom or
none.
- Put the logo file in the look's folder and name it in
set.json, so Plan can show it:"logo": "logo.svg". Use an official file unaltered, and only one you may share. - The simple way: add
logo: new URL('./logo.png', import.meta.url).hreftoset.js. The graphics then show it in the logo boxes, where an uploaded logo would go. Standard, Bold, Brutal, Signal and Contours carry the Split Control logo this way. - Or place it yourself (IOF and Svensk Orientering do): leave
logoout ofset.jsand draw it intheme.cssonly fortemplate, so a custom logo replaces it and "No logotype" hides it:.gfx[data-logo="template"] .panel--title .title-body::after { content: ""; position: absolute; right: 30px; top: 24px; width: 160px; height: 66px; background: url(./logo.svg) center / contain no-repeat; } - The uploaded logo goes into the logo boxes (
.hdr-logo,.title-logo,.ftr-logo,.clk-logo) only withcustom; style them as before.
Try all three with Logo in the preview.
Fonts
A look carries its own fonts. Production computers are often borrowed or freshly installed, so the look must not depend on fonts installed on the computer. If it does, it looks different on every machine, or falls back to Arial on air.
What goes in the look
my-look/
├── set.js fonts: points at fonts/fonts.css
├── theme.css --font: "Libre Franklin", Arial, sans-serif;
└── fonts/
├── fonts.css one @font-face per weight and style
├── libre-franklin-latin-600-normal.woff2
├── libre-franklin-latin-ext-600-normal.woff2
└── LICENSE-libre-franklin.txt
fonts/fonts.cssdeclares the fonts with@font-face, pointing at files next to it (url(./…woff2)).set.jsnames it asfonts. The graphics load it into the page for you:@font-facedoes not work inside a graphic's shadow DOM, so don't declare fonts intheme.css.- Font files: use
.woff2. Include the latin and latin-ext subsets so names like Ørjan, Hägström, Čech and Łukasz render in the same font. - Every weight you use: if
theme.cssasks for 800 and you only bundle 400, the browser fakes a bold that looks smeared on air. Bundle each weight the look uses (italics only if it uses them). - The licence file goes in
fonts/. Bundle only fonts you are allowed to share: SIL Open Font License (OFL) and Apache fonts are fine. Commercial fonts usually are not.
Font stacks
Put the bundled font first, then a common system font and a generic family:
.gfx {
--font: "Libre Franklin", Arial, sans-serif;
}
Don't put an installed font in front of the bundled one, even a brand font. Computers that have it (Franklin Gothic comes with Microsoft Office, for example) would show a different look from those that don't.
Brand fonts you can't share
Many federation and broadcaster fonts are commercial (ITC Franklin Gothic for the IOF,
GT Planar for ČO). Bundle a free stand-in that is close in shape and width, and say so in
set.json; the Looks page shows it:
"fonts": "Libre Franklin (OFL), bundled; it stands in for ITC Franklin Gothic"
A broadcaster who owns the brand font can make their own copy of the look with the font
files in fonts/, keeping the copy to themselves.
Where to get the files
Fontsource has most open fonts already split by
weight and subset, with ready-made @font-face rules: download the font, copy
the .woff2 files you need into fonts/, and change the
url()s to ./file.woff2. The looks in the repository were made this
way.
Check it
- Load the zip in the preview. In the browser's developer tools,
the Network tab should show your
.woff2files loading, with no 404s. - Check that numbers have tabular figures (running times shouldn't wobble) and that accented names use the same font.
- In the repository,
npm testfails if a look uses a font it doesn't bundle, points to a missing file, or loads files from another look.
Motion
The graphic sets data-state on .gfx; animate with CSS keyframes
for each state. The durations in set.js say how long each state lasts.
in | Entering (durations.in) |
on | On air. Live changes flash with .is-changed; rows slide to their new place. |
step-out / step | Changing page (durations.stepOut, durations.step) |
leave | Leaving (durations.out) |
out | Hidden |
Animate only transform and opacity: they stay smooth in NDI
capture. Don't follow the computer's Reduce Motion setting: broadcast output must look the
same on every machine.
The data graphics get
The app sends the same data to every look. The main fields:
results[] | Runners at the shown timing point: name, club, country, bib, position, time, diff, statusCode (0 OK, 1 DNS, 2 DNF, 3 MP, 4 DSQ), startTimeMs, splitTimes[] |
className, controlCode, splitControls[] | Class, timing point (S, a control code or F) and the class's radio controls |
runnerName, runnerClub, runnerBib | The runner in focus (lower thirds, identity, runner focus) |
title, series, place, date, text, name… | Fields the operator fills in |
presentation | language, identity (club, flag, both), themeColors, logoMode (template, custom, none), logoUrl (only with custom) |
clockOffsetMs | Server time minus this computer's time, for running clocks |
The preview's More data (JSON) box shows and edits these fields.
Test before race day
In the repository, run npm run type-check, npm test -- --run
and npm run test:templates. The last command loads every package in real
Chromium, exercises the lifecycle and variants, and writes screenshots and a JSON
report to the system temporary directory. Synthetic data and hidden rendering do not
replace a final check in Live or testing physical NDI/Companion outputs.
- Every graphic your look provides, on air and while changing page or focus step.
- Long names and clubs, accents, and 10+ runners (the results table pages).
- Clubs, flags, and both; English and Swedish labels.
- Live updates: run the simulator at 20× with a results table on air.
- Transparent and light backgrounds.
- In the app: Plan → Theme preview, then the web output in OBS or vMix.
- To rehearse with real timing data, replay a finished race.
Share a look
- As a zip: zip the look's folder (
my-look/at the top of the zip), includingfonts/. Anyone can import it in Graphics library → Import look…. - On this site: open a pull request adding your look to
templates/sets/. Every look there appears on the Looks page and in the preview.
Other OGraf graphics
Graphics that aren't part of a look also work: any
EBU OGraf v1 package (a folder with
<name>.ograf.json and its module) can be imported as a zip and appears in
Split Control's Graphics library. Split Control also speaks the OGraf Server API at
http://<computer>:5657/ograf/v1.