| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var CHAR_SETS = { |
| 5 |
alpha: ' ABCDEFGHIJKLMNOPQRSTUVWXYZ', |
| 6 |
alnum: ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', |
| 7 |
full: ' ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?:/-+@#$%&()', |
| 8 |
numeric: ' 0123456789' |
| 9 |
}; |
| 10 |
|
| 11 |
var FONT_FAMILIES = { |
| 12 |
mono: '"Courier New","Lucida Console",monospace', |
| 13 |
sans: '"Arial","Helvetica Neue",sans-serif', |
| 14 |
slab: '"Rockwell","Georgia",serif' |
| 15 |
}; |
| 16 |
|
| 17 |
var _typoKeys = [ |
| 18 |
['family','font-family'],['weight','font-weight'],['style','font-style'], |
| 19 |
['decoration','text-decoration'],['transform','text-transform'], |
| 20 |
['sizeDesktop','font-size-d'],['sizeTablet','font-size-t'],['sizeMobile','font-size-m'], |
| 21 |
['lineHeightDesktop','line-height-d'],['lineHeightTablet','line-height-t'],['lineHeightMobile','line-height-m'], |
| 22 |
['letterSpacingDesktop','letter-spacing-d'],['letterSpacingTablet','letter-spacing-t'],['letterSpacingMobile','letter-spacing-m'], |
| 23 |
['wordSpacingDesktop','word-spacing-d'],['wordSpacingTablet','word-spacing-t'],['wordSpacingMobile','word-spacing-m'] |
| 24 |
]; |
| 25 |
var _sizeKeys = {'font-size-d':1,'font-size-t':1,'font-size-m':1}; |
| 26 |
var _lhKeys = {'line-height-d':1,'line-height-t':1,'line-height-m':1}; |
| 27 |
var _lsKeys = {'letter-spacing-d':1,'letter-spacing-t':1,'letter-spacing-m':1}; |
| 28 |
var _wsKeys = {'word-spacing-d':1,'word-spacing-t':1,'word-spacing-m':1}; |
| 29 |
|
| 30 |
function typoCssVarsForEl(el, obj, prefix) { |
| 31 |
if (!obj) return; |
| 32 |
_typoKeys.forEach(function (pair) { |
| 33 |
var v = obj[pair[0]]; |
| 34 |
if (v === undefined || v === '' || v === null) return; |
| 35 |
var unit = ''; |
| 36 |
if (_sizeKeys[pair[1]]) unit = obj.sizeUnit || 'px'; |
| 37 |
else if (_lhKeys[pair[1]]) unit = obj.lineHeightUnit || ''; |
| 38 |
else if (_lsKeys[pair[1]]) unit = obj.letterSpacingUnit || 'px'; |
| 39 |
else if (_wsKeys[pair[1]]) unit = obj.wordSpacingUnit || 'px'; |
| 40 |
el.style.setProperty(prefix + pair[1], (typeof v === 'number' ? v + unit : v)); |
| 41 |
}); |
| 42 |
} |
| 43 |
|
| 44 |
function initSplitFlap(appEl) { |
| 45 |
var raw = appEl.dataset.opts; |
| 46 |
if (!raw) return; |
| 47 |
var opts; |
| 48 |
try { opts = JSON.parse(raw); } catch (e) { return; } |
| 49 |
|
| 50 |
var messages = (opts.messages || ['DEPARTURES']).map(function (m) { return m.toUpperCase(); }); |
| 51 |
var charSet = CHAR_SETS[opts.charSet || 'alpha']; |
| 52 |
var font = FONT_FAMILIES[opts.fontFamily || 'mono']; |
| 53 |
|
| 54 |
var cw = opts.charWidth || 48; |
| 55 |
var ch = opts.charHeight || 64; |
| 56 |
var cg = opts.charGap || 4; |
| 57 |
var rg = opts.rowGap || 8; |
| 58 |
var fs = opts.fontSize || 28; |
| 59 |
var fw = opts.fontWeight || 700; |
| 60 |
var br = opts.borderRadius || 6; |
| 61 |
var speed = opts.flipSpeed || 60; |
| 62 |
var stagger = opts.flipStagger || 30; |
| 63 |
|
| 64 |
// Pad all messages to the same length (max length) |
| 65 |
var maxLen = 0; |
| 66 |
messages.forEach(function (m) { if (m.length > maxLen) maxLen = m.length; }); |
| 67 |
messages = messages.map(function (m) { |
| 68 |
while (m.length < maxLen) m += ' '; |
| 69 |
return m; |
| 70 |
}); |
| 71 |
|
| 72 |
var msgIdx = 0; |
| 73 |
var flipping = false; |
| 74 |
|
| 75 |
// Build the board DOM |
| 76 |
var wrap = document.createElement('div'); |
| 77 |
wrap.className = 'bkbg-sfd-wrap'; |
| 78 |
|
| 79 |
var board = document.createElement('div'); |
| 80 |
board.className = 'bkbg-sfd-board'; |
| 81 |
board.style.backgroundColor = opts.boardBg || '#1a1a1a'; |
| 82 |
board.style.borderRadius = (opts.boardRadius || 12) + 'px'; |
| 83 |
board.style.padding = (opts.boardPadding || 24) + 'px'; |
| 84 |
board.style.display = 'inline-block'; |
| 85 |
|
| 86 |
if (opts.showLabel) { |
| 87 |
var lbl = document.createElement('div'); |
| 88 |
lbl.className = 'bkbg-sfd-label'; |
| 89 |
lbl.textContent = opts.label || 'FLIGHT INFORMATION'; |
| 90 |
lbl.style.color = opts.labelColor || '#888'; |
| 91 |
board.appendChild(lbl); |
| 92 |
} |
| 93 |
|
| 94 |
var rowsEl = document.createElement('div'); |
| 95 |
rowsEl.className = 'bkbg-sfd-rows'; |
| 96 |
rowsEl.style.gap = rg + 'px'; |
| 97 |
|
| 98 |
// Single display row (shows one message at a time) |
| 99 |
var rowEl = document.createElement('div'); |
| 100 |
rowEl.className = 'bkbg-sfd-row'; |
| 101 |
rowEl.style.gap = cg + 'px'; |
| 102 |
|
| 103 |
// Current displayed chars |
| 104 |
var currentChars = new Array(maxLen).fill(' '); |
| 105 |
var charEls = []; |
| 106 |
|
| 107 |
function makeCharEl(ch) { |
| 108 |
var tile = document.createElement('div'); |
| 109 |
tile.className = 'bkbg-sfd-char'; |
| 110 |
tile.style.width = cw + 'px'; |
| 111 |
tile.style.height = ch + 'px'; |
| 112 |
tile.style.backgroundColor = ch === ' ' ? 'transparent' : (opts.charBg || '#2a2a2a'); |
| 113 |
tile.style.borderRadius = br + 'px'; |
| 114 |
tile.style.perspective = '400px'; |
| 115 |
tile.style.setProperty('--sfd-speed', speed + 'ms'); |
| 116 |
if (opts.glowColor) tile.style.boxShadow = '0 0 12px ' + opts.glowColor; |
| 117 |
|
| 118 |
// Top half |
| 119 |
var topEl = document.createElement('div'); |
| 120 |
topEl.className = 'bkbg-sfd-top'; |
| 121 |
topEl.style.backgroundColor = opts.charBg || '#2a2a2a'; |
| 122 |
var topTxt = document.createElement('span'); |
| 123 |
topTxt.className = 'bkbg-sfd-char-text'; |
| 124 |
topTxt.style.color = opts.charColor || '#f5f5dc'; |
| 125 |
topTxt.style.paddingBottom = '0'; |
| 126 |
topTxt.style.lineHeight = (ch * 2) + 'px'; // so bottom half is cropped |
| 127 |
topTxt.textContent = ch; |
| 128 |
topEl.appendChild(topTxt); |
| 129 |
tile.appendChild(topEl); |
| 130 |
|
| 131 |
// Bottom half |
| 132 |
var botEl = document.createElement('div'); |
| 133 |
botEl.className = 'bkbg-sfd-bottom'; |
| 134 |
botEl.style.backgroundColor = opts.flapHalfBg || '#222'; |
| 135 |
var botTxt = document.createElement('span'); |
| 136 |
botTxt.className = 'bkbg-sfd-char-text'; |
| 137 |
botTxt.style.color = opts.charColor || '#f5f5dc'; |
| 138 |
botTxt.style.lineHeight = '0'; // top half crops in |
| 139 |
botTxt.textContent = ch; |
| 140 |
botEl.appendChild(botTxt); |
| 141 |
tile.appendChild(botEl); |
| 142 |
|
| 143 |
// Divider |
| 144 |
var divEl = document.createElement('div'); |
| 145 |
divEl.className = 'bkbg-sfd-divider'; |
| 146 |
divEl.style.background = opts.dividerColor || '#111'; |
| 147 |
tile.appendChild(divEl); |
| 148 |
|
| 149 |
tile._topTxt = topTxt; |
| 150 |
tile._botTxt = botTxt; |
| 151 |
tile._topEl = topEl; |
| 152 |
tile._botEl = botEl; |
| 153 |
|
| 154 |
return tile; |
| 155 |
} |
| 156 |
|
| 157 |
for (var i = 0; i < maxLen; i++) { |
| 158 |
var tile = makeCharEl(messages[0][i] || ' '); |
| 159 |
currentChars[i] = messages[0][i] || ' '; |
| 160 |
charEls.push(tile); |
| 161 |
rowEl.appendChild(tile); |
| 162 |
} |
| 163 |
|
| 164 |
rowsEl.appendChild(rowEl); |
| 165 |
board.appendChild(rowsEl); |
| 166 |
|
| 167 |
// Dots (ticker indicator) |
| 168 |
var dotsEl = null; |
| 169 |
if (messages.length > 1) { |
| 170 |
dotsEl = document.createElement('div'); |
| 171 |
dotsEl.className = 'bkbg-sfd-dots'; |
| 172 |
messages.forEach(function (_, di) { |
| 173 |
var dot = document.createElement('div'); |
| 174 |
dot.className = 'bkbg-sfd-dot' + (di === 0 ? ' bkbg-sfd-dot-active' : ''); |
| 175 |
dot.style.backgroundColor = opts.charColor || '#f5f5dc'; |
| 176 |
dotsEl.appendChild(dot); |
| 177 |
}); |
| 178 |
board.appendChild(dotsEl); |
| 179 |
} |
| 180 |
|
| 181 |
wrap.appendChild(board); |
| 182 |
|
| 183 |
/* ── Apply typography CSS vars on wrap ──────────────────────── */ |
| 184 |
wrap.style.setProperty('--bksfd-ch-font-family', font); |
| 185 |
wrap.style.setProperty('--bksfd-ch-font-size-d', fs + 'px'); |
| 186 |
wrap.style.setProperty('--bksfd-ch-font-weight', String(fw)); |
| 187 |
wrap.style.setProperty('--bksfd-lb-font-family', font); |
| 188 |
typoCssVarsForEl(wrap, opts.charTypo, '--bksfd-ch-'); |
| 189 |
typoCssVarsForEl(wrap, opts.labelTypo, '--bksfd-lb-'); |
| 190 |
|
| 191 |
appEl.parentNode.replaceChild(wrap, appEl); |
| 192 |
|
| 193 |
// ── Flip animation ───────────────────────────────────────────── |
| 194 |
function flipCharTo(tile, fromChar, toChar, delay, onDone) { |
| 195 |
if (fromChar === toChar) { if (onDone) onDone(); return; } |
| 196 |
setTimeout(function () { |
| 197 |
var idx = charSet.indexOf(fromChar); |
| 198 |
if (idx === -1) idx = 0; |
| 199 |
|
| 200 |
// Create flipping top flap (shows fromChar top, folds down) |
| 201 |
var flipTopEl = document.createElement('div'); |
| 202 |
flipTopEl.className = 'bkbg-sfd-flip-top'; |
| 203 |
flipTopEl.style.backgroundColor = opts.charBg || '#2a2a2a'; |
| 204 |
flipTopEl.style.setProperty('--sfd-speed', speed + 'ms'); |
| 205 |
var ftTxt = document.createElement('span'); |
| 206 |
ftTxt.className = 'bkbg-sfd-char-text'; |
| 207 |
ftTxt.style.color = opts.charColor || '#f5f5dc'; |
| 208 |
ftTxt.style.lineHeight = (ch * 2) + 'px'; |
| 209 |
ftTxt.textContent = fromChar; |
| 210 |
flipTopEl.appendChild(ftTxt); |
| 211 |
|
| 212 |
// Create flipping bottom flap (shows toChar bottom, unfolds) |
| 213 |
var flipBotEl = document.createElement('div'); |
| 214 |
flipBotEl.className = 'bkbg-sfd-flip-bottom'; |
| 215 |
flipBotEl.style.backgroundColor = opts.flapHalfBg || '#222'; |
| 216 |
flipBotEl.style.setProperty('--sfd-speed', speed + 'ms'); |
| 217 |
var fbTxt = document.createElement('span'); |
| 218 |
fbTxt.className = 'bkbg-sfd-char-text'; |
| 219 |
fbTxt.style.color = opts.charColor || '#f5f5dc'; |
| 220 |
fbTxt.style.lineHeight = '0'; |
| 221 |
fbTxt.textContent = toChar; |
| 222 |
flipBotEl.appendChild(fbTxt); |
| 223 |
|
| 224 |
// Update underlying halves immediately to target char |
| 225 |
tile._topTxt.textContent = toChar; |
| 226 |
tile._botTxt.textContent = toChar; |
| 227 |
|
| 228 |
tile.appendChild(flipTopEl); |
| 229 |
tile.appendChild(flipBotEl); |
| 230 |
|
| 231 |
// Remove after animation |
| 232 |
setTimeout(function () { |
| 233 |
if (flipTopEl.parentNode) flipTopEl.parentNode.removeChild(flipTopEl); |
| 234 |
if (flipBotEl.parentNode) flipBotEl.parentNode.removeChild(flipBotEl); |
| 235 |
if (onDone) onDone(); |
| 236 |
}, speed + 20); |
| 237 |
}, delay); |
| 238 |
} |
| 239 |
|
| 240 |
function flipToMessage(target) { |
| 241 |
if (flipping) return; |
| 242 |
flipping = true; |
| 243 |
var remaining = maxLen; |
| 244 |
|
| 245 |
for (var i = 0; i < maxLen; i++) { |
| 246 |
(function (ci) { |
| 247 |
var from = currentChars[ci] || ' '; |
| 248 |
var to = (target[ci] || ' '); |
| 249 |
flipCharTo(charEls[ci], from, to, ci * stagger, function () { |
| 250 |
remaining--; |
| 251 |
if (remaining === 0) { |
| 252 |
flipping = false; |
| 253 |
currentChars = target.split(''); |
| 254 |
} |
| 255 |
}); |
| 256 |
})(i); |
| 257 |
} |
| 258 |
} |
| 259 |
|
| 260 |
// ── Ticker logic ─────────────────────────────────────────────── |
| 261 |
function doTicker() { |
| 262 |
if (messages.length < 2) return; |
| 263 |
msgIdx = (msgIdx + 1) % messages.length; |
| 264 |
flipToMessage(messages[msgIdx]); |
| 265 |
|
| 266 |
// Update dots |
| 267 |
if (dotsEl) { |
| 268 |
var dots = dotsEl.querySelectorAll('.bkbg-sfd-dot'); |
| 269 |
dots.forEach(function (d, di) { |
| 270 |
d.classList.toggle('bkbg-sfd-dot-active', di === msgIdx); |
| 271 |
}); |
| 272 |
} |
| 273 |
} |
| 274 |
|
| 275 |
var tickerTimer = null; |
| 276 |
|
| 277 |
function startTicker() { |
| 278 |
if (tickerTimer) return; |
| 279 |
tickerTimer = setInterval(doTicker, opts.tickerDelay || 3000); |
| 280 |
} |
| 281 |
|
| 282 |
// ── Scroll trigger ───────────────────────────────────────────── |
| 283 |
if (opts.flipOnScroll) { |
| 284 |
var played = false; |
| 285 |
var observer = new IntersectionObserver(function (entries) { |
| 286 |
if (entries[0].isIntersecting && !played) { |
| 287 |
played = true; |
| 288 |
flipToMessage(messages[0]); |
| 289 |
if (opts.tickerMode) { |
| 290 |
setTimeout(startTicker, (opts.tickerDelay || 3000)); |
| 291 |
} |
| 292 |
} |
| 293 |
}, { threshold: 0.3 }); |
| 294 |
observer.observe(wrap); |
| 295 |
} else if (opts.autoPlay || opts.tickerMode) { |
| 296 |
flipToMessage(messages[0]); |
| 297 |
if (opts.tickerMode) { |
| 298 |
setTimeout(startTicker, opts.tickerDelay || 3000); |
| 299 |
} |
| 300 |
} |
| 301 |
} |
| 302 |
|
| 303 |
document.addEventListener('DOMContentLoaded', function () { |
| 304 |
document.querySelectorAll('.bkbg-sfd-app').forEach(initSplitFlap); |
| 305 |
}); |
| 306 |
|
| 307 |
if (document.readyState === 'complete' || document.readyState === 'interactive') { |
| 308 |
document.querySelectorAll('.bkbg-sfd-app').forEach(initSplitFlap); |
| 309 |
} |
| 310 |
})(); |
| 311 |
|