flipoff-split-flap-display

Expert skill for building, customizing, and embedding the FlipOff split-flap display emulator — a free, offline-capable web app that turns any browser/TV into…

INSTALLATION
npx skills add https://github.com/aradotso/trending-skills --skill flipoff-split-flap-display
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

FlipOff Split-Flap Display Emulator

Skill by ara.so — Daily 2026 Skills collection.

FlipOff is a pure vanilla HTML/CSS/JS web app that emulates classic mechanical split-flap (flip-board) airport displays. No frameworks, no npm, no build step — open index.html and you have a full-screen retro display with authentic scramble animations and clacking sounds.

Installation

git clone https://github.com/magnum6actual/flipoff.git

cd flipoff

# Option 1: Open directly

open index.html

Option 2: Serve locally (recommended for audio)

python3 -m http.server 8080

Visit http://localhost:8080

> **Audio note:** Browsers block autoplay. The user must click once to enable the Web Audio API context. After that, sound plays automatically on each message transition.

---

## File Structure

flipoff/

index.html — Single-page app entry point

css/

reset.css — CSS reset

layout.css — Header, hero, page layout

board.css — Board container and accent bars

tile.css — Tile styling and 3D flip animation

responsive.css — Media queries (mobile → 4K)

js/

main.js — Entry point, wires everything together

Board.js — Grid manager, transition orchestration

Tile.js — Per-tile animation logic

SoundEngine.js — Web Audio API playback

MessageRotator.js — Auto-rotate timer

KeyboardController.js — Keyboard shortcut handling

constants.js — All configuration lives here

flapAudio.js — Base64-encoded audio data

---

## Key Configuration — `js/constants.js`

Everything you'd want to change lives in one file:

// js/constants.js (representative structure)

export const GRID_COLS = 26; // Characters per row

export const GRID_ROWS = 8; // Number of rows

export const SCRAMBLE_DURATION = 600; // ms each tile scrambles before settling

export const STAGGER_DELAY = 18; // ms between each tile starting its scramble

export const AUTO_ROTATE_INTERVAL = 8000; // ms between auto-advancing messages

export const SCRAMBLE_COLORS = [

'#FF6B35', '#F7C59F', '#EFEFD0',

'#004E89', '#1A936F', '#C6E0F5'

];

export const ACCENT_COLORS = ['#FF6B35', '#004E89', '#1A936F'];

export const MESSAGES = [

"HAVE A NICE DAY",

"ALL FLIGHTS ON TIME",

"WELCOME TO THE FUTURE",

// Add your own here

];


## Adding Custom Messages

Edit `MESSAGES` in `js/constants.js`. Each message is a plain string. The board wraps text across the grid automatically.

export const MESSAGES = [

"DEPARTING GATE 7",

"YOUR COFFEE IS READY",

"BUILD THINGS THAT MATTER",

"FLIGHT AA 404 NOT FOUND", // max GRID_COLS * GRID_ROWS chars

];


**Padding rules:** Messages shorter than the grid are padded with spaces. Messages longer than the grid are truncated. Keep messages at or under `GRID_COLS × GRID_ROWS` characters.

## Changing Grid Size

// Compact 16×4 ticker-style board

export const GRID_COLS = 16;

export const GRID_ROWS = 4;

// Wide cinema board

export const GRID_COLS = 40;

export const GRID_ROWS = 6;

// Tall info kiosk

export const GRID_COLS = 20;

export const GRID_ROWS = 12;


After changing grid dimensions, tiles re-render automatically on next page load.

## Keyboard Shortcuts

Key
Action

`Enter` / `Space`
Next message

`Arrow Left`
Previous message

`Arrow Right`
Next message

`F`
Toggle fullscreen

`M`
Toggle mute

`Escape`
Exit fullscreen

## Programmatic API

### Board

// Board.js exposes a class you can instantiate directly

import Board from './js/Board.js';

const board = new Board(document.getElementById('board-container'));

// Display a specific string

board.setMessage('GATE CHANGE B12');

// Advance to next message in the rotation

board.next();

// Go back

board.previous();


### MessageRotator

import MessageRotator from './js/MessageRotator.js';

const rotator = new MessageRotator(board, messages, AUTO_ROTATE_INTERVAL);

rotator.start(); // begin auto-advancing

rotator.stop(); // pause rotation

rotator.next(); // manual advance

rotator.previous(); // manual back


### SoundEngine

import SoundEngine from './js/SoundEngine.js';

const sound = new SoundEngine();

// Must call after a user gesture (click/keypress)

await sound.init();

sound.play(); // play the flap transition sound

sound.mute(); // silence

sound.unmute();

sound.toggle(); // flip mute state


### KeyboardController

import KeyboardController from './js/KeyboardController.js';

const kb = new KeyboardController({

onNext: () => rotator.next(),

onPrevious: () => rotator.previous(),

onFullscreen: () => toggleFullscreen(),

onMute: () => sound.toggle(),

});

kb.attach(); // start listening

kb.detach(); // stop listening


## Embedding FlipOff in Another Page

### As an iframe

<iframe

src="/flipoff/index.html"

width="1280"

height="400"

style="border:none; background:#000;"

allowfullscreen

></iframe>


### Inline embed (pull in just the board)

<!-- In your page -->

<div id="flip-board"></div>

<script type="module">

import Board from '/flipoff/js/Board.js';

import SoundEngine from '/flipoff/js/SoundEngine.js';

import { MESSAGES, AUTO_ROTATE_INTERVAL } from '/flipoff/js/constants.js';

const board = new Board(document.getElementById('flip-board'));

const sound = new SoundEngine();

let idx = 0;

board.setMessage(MESSAGES[idx]);

document.addEventListener('click', async () => {

await sound.init();

}, { once: true });

setInterval(() => {

idx = (idx + 1) % MESSAGES.length;

board.setMessage(MESSAGES[idx]);

sound.play();

}, AUTO_ROTATE_INTERVAL);

</script>


## Custom Color Themes

// js/constants.js — dark blue terminal theme

export const SCRAMBLE_COLORS = [

'#0D1B2A', '#1B2838', '#00FF41',

'#003459', '#007EA7', '#00A8E8'

];

export const ACCENT_COLORS = ['#00FF41', '#007EA7', '#00A8E8'];

/ css/board.css — override tile background /

.tile {

background-color: #0D1B2A;

color: #00FF41;

border-color: #003459;

}


## Common Patterns

### Show real-time data (e.g., a flight API)

import Board from './js/Board.js';

import SoundEngine from './js/SoundEngine.js';

const board = new Board(document.getElementById('board'));

const sound = new SoundEngine();

async function fetchAndDisplay() {

const res = await fetch('/api/departures');

const data = await res.json();

const message = ${data.flight} ${data.destination} ${data.gate};

board.setMessage(message.toUpperCase());

sound.play();

}

document.addEventListener('click', () => sound.init(), { once: true });

setInterval(fetchAndDisplay, 30_000);

fetchAndDisplay();


### Cycle through a custom message list

const promos = [

"SALE ENDS SUNDAY",

"FREE SHIPPING OVER $50",

"NEW ARRIVALS THIS WEEK",

];

let i = 0;

setInterval(() => {

board.setMessage(promos[i % promos.length]);

sound.play();

i++;

}, 5000);


### React/Vue wrapper (import as a module)

// FlipBoard.jsx

import { useEffect, useRef } from 'react';

import Board from '../flipoff/js/Board.js';

import { MESSAGES } from '../flipoff/js/constants.js';

export default function FlipBoard({ messages = MESSAGES, interval = 8000 }) {

const containerRef = useRef(null);

const boardRef = useRef(null);

useEffect(() => {

boardRef.current = new Board(containerRef.current);

let idx = 0;

boardRef.current.setMessage(messages[idx]);

const timer = setInterval(() => {

idx = (idx + 1) % messages.length;

boardRef.current.setMessage(messages[idx]);

}, interval);

return () => clearInterval(timer);

}, []);

return <div ref={containerRef} className="flip-board-container" />;

}


## Troubleshooting

Problem
Fix

No sound
User must click/interact first; Web Audio requires a user gesture

Sound works locally but not deployed
Ensure `flapAudio.js` (base64) is served; check MIME types

Tiles don't animate
Verify CSS `tile.css` is loaded; check for JS console errors

Grid overflows on small screens
Reduce `GRID_COLS`/`GRID_ROWS` in `constants.js` or add CSS `overflow: hidden`

Fullscreen not working
`F` key calls `requestFullscreen()` — some browsers require the page to be focused

Messages cut off
String length exceeds `GRID_COLS × GRID_ROWS`; shorten or increase grid size

Audio blocked by CSP
Add `media-src 'self' blob: data:` to your Content-Security-Policy

CORS error loading modules
Serve with a local server (`python3 -m http.server`), not `file://`

## Tips for TV / Kiosk Deployment

Serve with a simple static server

npx serve . # Node

python3 -m http.server # Python

Auto-launch fullscreen in Chromium kiosk mode

chromium-browser --kiosk --app=http://localhost:8080

Hide cursor after idle (add to index.html)

document.addEventListener('mousemove', () => {

document.body.style.cursor = 'default';

clearTimeout(window._cursorTimer);

window._cursorTimer = setTimeout(() => {

document.body.style.cursor = 'none';

}, 3000);

});

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card