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 a retro airport departure board.
How do I install this agent skill?
npx skills add https://github.com/aradotso/trending-skills --skill flipoff-split-flap-displayIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill is safe to use. It provides instructions and code examples for setting up a web-based split-flap display emulator. All identified external resources and commands are standard for web development and tool deployment.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 1 issue
- ZeroLeakspass
2 findings · Score: 80/100
What does this agent skill do?
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);
});
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/aradotso/trending-skills/flipoff-split-flap-display">View flipoff-split-flap-display on skillZs</a>