clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / build / linux / steam-art / art.js
art.js 3.5K
1// Builds the faint guide-grid motif used across the Steam library art.2// Deterministic (seeded) so re-renders come out identical.34function rng(seed) {5 return () => {6 seed = (seed + 0x6d2b79f5) | 0;7 let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);8 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;9 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;10 };11}1213const CHANNELS = ["2.1", "4.1", "5.1", "7.1", "9.1", "11.1", "13.1", "20.1", "28.1", "32.1", "38.1", "44.1", "50.1", "62.1"];14// Program lengths in half hours: mostly half-hour and hour shows, the odd movie.15const LENGTHS = [1, 1, 1, 1, 2, 2, 2, 1, 3, 4];1617function clock(min) {18 const h = Math.floor(min / 60) % 12 || 12;19 const m = String(min % 60).padStart(2, "0");20 return `${h}:${m}`;21}2223// o: { u: scale (1 = the app's 58px rows), chw: channel column width in app px (0 = none),24// slot: px per half hour in app px, now: now position in half hours from the lane start,25// ticks: show the time row, t0: minutes since midnight at the lane start,26// first: index into CHANNELS, cur: row index of the tuned channel, sel: highlight its program,27// seed }28export function guide(el, o) {29 const u = o.u ?? 1;30 el.style.setProperty("--u", u);31 const W = el.clientWidth, H = el.clientHeight;32 const rowH = 58 * u, tickH = o.ticks ? 34 * u : 0, chw = (o.chw ?? 0) * u;33 const slot = (o.slot ?? 180) * u, gap = 3 * u;34 const nowX = chw + (o.now ?? 1.4) * slot;35 const rand = rng(o.seed ?? 7);3637 if (o.ticks) {38 const ticks = document.createElement("div");39 ticks.className = "ticks";40 for (let i = 0, x = chw; x < W; i++, x += slot) {41 const s = document.createElement("span");42 s.style.left = `${x}px`;43 s.textContent = clock((o.t0 ?? 19 * 60) + i * 30);44 ticks.appendChild(s);45 }46 el.appendChild(ticks);47 }4849 const rows = Math.ceil((H - tickH) / rowH);50 for (let i = 0; i < rows; i++) {51 const row = document.createElement("div");52 row.className = "row" + (i === o.cur ? " cur" : "");53 row.style.top = `${tickH + i * rowH}px`;54 row.style.height = `${rowH}px`;55 if (chw > 0) {56 const ch = document.createElement("div");57 ch.className = "ch";58 ch.style.width = `${chw}px`;59 ch.textContent = CHANNELS[((o.first ?? 0) + i) % CHANNELS.length];60 row.appendChild(ch);61 }62 // Programs start on half-hour boundaries; the first one usually began before the window.63 let x = chw - Math.floor(rand() * 3) * slot;64 while (x < W) {65 const len = LENGTHS[Math.floor(rand() * LENGTHS.length)] * slot;66 const left = Math.max(x, chw), right = x + len - gap;67 if (right > left) {68 const c = document.createElement("div");69 c.className = "cell";70 if (right < nowX) c.classList.add("past");71 else if (x <= nowX) c.classList.add(i === o.cur && o.sel ? "sel" : "now");72 c.style.left = `${left}px`;73 c.style.width = `${right - left}px`;74 row.appendChild(c);75 }76 x += len;77 }78 el.appendChild(row);79 }8081 const line = document.createElement("div");82 line.className = "now-line";83 line.style.left = `${nowX}px`;84 el.appendChild(line);85}8687// Resolves once the art's fonts are in. Pages set body[data-ready] when drawn.88export async function ready() {89 await document.fonts.load('800 100px "Shoulders"');90 await document.fonts.load('600 100px "Shoulders"');91 await document.fonts.load('400 12px "Plex Mono"');92 await document.fonts.load('500 12px "Plex Mono"');93 await document.fonts.ready;94}