type Times = number[]; type Cats = [string, Times]; const MATRIX: Cats[] = [ ["o0", [1, 5, 8, 9, 11]], ["o1", [2, 5, 6, 10, 11]], ["o2", [3, 6, 7, 9, 11]], ["o3", [4, 7, 8, 10, 11]], ]; const TEMPLATE = `
`; type Face = "hours" | "minutes" | "seconds"; const isFace = (v: unknown): v is Face => { if (typeof v === "string" && ["hours", "minutes", "seconds"].includes(v)) { return true; } throw new Error(`Expected a Face, got ${v}`); }; class DominoClock extends HTMLElement { elements: { [K in Face]: HTMLDivElement }; timer: number; constructor() { super(); this.timer = 0; this.innerHTML = TEMPLATE; this.elements = Object.fromEntries( Array.from(this.getElementsByTagName("div")) .filter(element => element.className === "face") .map(element => [isFace(element.id) && element.id, element]) ); this.paint = this.paint.bind(this); } connectedCallback() { this.timer = window.setTimeout(this.paint, 250); } disconnectedCallback() { window.clearTimeout(this.timer); } tock(board: Face, time: number) { const dots = Array.from(this.elements[board].getElementsByTagName("div")); dots.forEach(element => { element.style.opacity = "0"; }); dots.forEach(element => { const row = MATRIX.find(i => Array.from(element.classList).includes(i[0])); if (!row) { console.log(`Didn't find a row for ${element.className}?`); return; } if (row[1].includes(time)) { element.style.opacity = "1"; } }); } paint() { const now = new Date(); const rawHours = now.getHours(); const hours = rawHours > 12 ? rawHours - 12 : rawHours; this.tock("hours", hours); this.tock("minutes", Math.floor(now.getUTCMinutes() / 5)); this.tock("seconds", Math.floor(now.getUTCSeconds() / 5)); window.clearTimeout(this.timer); this.timer = window.setTimeout(this.paint, 250); } } customElements.define("domino-clock", DominoClock);