Kiosk Display

The kiosk/ directory contains a standalone page for Samsung SmartTVs that rotates between different content screens. It runs on older Tizen browsers (2018+) in landscape mode.

Files

Multi-Screen Rotation System

All screens are sibling <div>s inside <body>. Only one is visible at a time (toggled via display: block / display: none). A central rotation loop cycles through a playlist.

body
  ├── #screen-events   (two-pane event display)
  ├── #screen-special  (full-bleed takeover for major events)
  └── #screen-welcome  (logo + sine-scrolling text)

Playlist order

With no special events, the playlist is:

Welcome → Event 0 → Event 1 → … → Event N → Welcome → Event 0 → …

When one or more events are flagged special: true, those events are pulled out of the normal rotation and the takeover takes every other slot – so it is on screen half the time:

Welcome → Special → Event 0 → Special → Event 1 → Special → … → Event N → Special → (wraps)

The cycle starts on a non-special and ends on a special, so the alternation holds across the wrap back to the welcome screen too.

Which act a given special slot shows is not fixed in the playlist – it is taken from a running cursor (specialActCursor) at display time. That keeps the three acts evenly distributed however many slots a cycle happens to contain; baking the act into the playlist starves one whenever the count is not a multiple of three.

Each screen shows for 15 seconds (SCREEN_DURATION_MS). The welcome screen appears once per full cycle.

Adding a new screen

  1. Add <div id="screen-foo" class="screen"> in index.html
  2. Add CSS for it in style.css
  3. Register it in the screens object in kiosk.js with activate / deactivate functions
  4. Add it to the playlist in buildPlaylist()

Current screens

Welcome screen (#screen-welcome): Displays the StartupOulu logo with a demoscene-style sine-wave text scroller. The text scrolls right-to-left with each character’s Y-coordinate animated via Math.sin(). A few 🦄 emoji drift across behind it. Animation starts on activate, cleans up on deactivate.

These unicorns are genuine emoji and render correctly on the TV — this screen inherits the plain body font stack. Do not “fix” them into SVGs by analogy with the special screen; see the emoji note under Technical Constraints.

Events screen (#screen-events): Two-pane layout showing event details (title, date, location, description, countdown) on the left and the event cover image with a QR code on the right.

Special takeover (#screen-special): Full-bleed screen for major events, used for any event with special: true in its front matter. Designed to be readable by someone walking past, so it leans on colour, scale and motion.

Persistent across every act:

One DOM, three acts swapped by JS, each opening with a short entry animation:

Act Content
0 Hype – giant uppercase title over a gradient rule. The headline is split into one span per character and twinkles: ~7% of letters are lit in a brand colour at any moment, the rest stay Canvas white. Characters are grouped into .sp-word nowrap wrappers so lines still break between words, not mid-word
1 Countdown – days / hours / min cards with a solid orange ticking seconds chip, or NYT MENOSSA · LIVE NOW while running
2 CTA – large scannable QR built from the event’s own cta_link, with cta_title as the headline

Timers used: specialCountdownTimer (act 1), specialRailTimer (rail pill), specialTickerTimer, specialBgTimer (flyby, exhaust, flaps), specialTwinkleTimer (headline), specialEntryTimer. All are cleared in deactivateSpecial, which runs on every screen transition — which also force-hides the ride, moon and exhaust, so a transition mid-flyby can never leave them stranded on screen.

Tuning the motion

The screen is deliberately calm. Earlier revisions had five continuous effects competing, and everything that repainted a large area every frame — the drifting orbs, the gradient light sweep, the full-screen act wipe — has since been removed because the TV could not keep up. If it needs rebalancing, these are the knobs (all in kiosk.js):

Constant Now Effect
TWINKLE_PER_TICK / TWINKLE_LIFE / TWINKLE_FPS 1 / 4 / 240 How many headline letters are lit at once (~7%)
FLIGHT_GAP_MIN_MS / FLIGHT_GAP_MAX_MS 3000 / 5000 Gap between flybys
FLAP_HALF 4 Frames per half-flip on the countdown board (~265 ms total)
FLIGHT_FRAMES_MIN / FLIGHT_FRAMES_VAR 56 / 26 Crossing duration (~1.8–2.7 s)
FUME_PER_FRAME / FUME_POOL 3 / 46 Exhaust density and pool ceiling

The ride flies behind the act content: #sp-rocket and #sp-fumes sit at z-index: 1, below the stage at 2, so a crossing never obscures the headline. Raising them above 2 would put the ride in front.

The ride is 30vh. Its size is declared twice — width/height on #sp-ride-img in the stylesheet and SHIP_VH in kiosk.js, which is where the exhaust spawn point is derived from. Change both together, or the plume detaches from the tail. The flight path is also tuned to that height: a 30vh ride on a steeper path would dip into the ticker band along the bottom.

Both dimensions are always stated explicitly. The artwork is square and viewBox-only, and a legacy engine that cannot infer an SVG’s intrinsic ratio will size it from its container instead — which is how a stale-CSS deploy once put a screen-filling logo over everything.

Prefer giving the flyby more presence over adding ambient motion — ambient motion habituates, onset does not. Whatever is added, keep it off large gradient-filled layers: those are what made this screen unusable on the TV.

This screen uses the brand palette (Knowledge #070540, Passion #FF3296, Fame #FF4600, Canvas #EDF2F5) and Hanken Grotesk. The webfont is loaded globally but applied only under #screen-special, so the other screens are unaffected.

A finished event loses its special treatment. hasEnded() gates the flag, so once end_time has passed the event is partitioned as an ordinary event and shows the normal two-pane slide — a finished event should not still be taking over the screen with a hype countdown. The playlist is only rebuilt every EVENTS_REFRESH_MS, so activateSpecial re-checks and rebuilds on the spot if an event finished while it still held special slots.

To make an event use this screen, add to its front matter:

special: true

events.json.liquid must emit the flag (it does) or the kiosk cannot see it.

Technical Constraints

Analytics

Testing

  1. bundle exec jekyll serve → open /kiosk/
  2. Welcome screen appears first with sine-scrolling text
  3. After 15s it advances. With no special event that is the first event slide; with one, it is the takeover, which then takes every other slot
  4. The cycle ends on the last slot and wraps back to welcome
  5. Test at 1280×720 and 1920×1080

The full rotation is long. To check one screen, pin it with ?only= rather than waiting the cycle out.

Pinning a single screen

Waiting out the rotation to check one screen is painful, so ?only= pins one:

/kiosk/?only=welcome
/kiosk/?only=special&act=0     # act 0, 1 or 2
/kiosk/?only=events&i=2        # event at index 2

Without ?only= the kiosk rotates normally.