Spaces:
Sleeping
Sleeping
| const t = document.getElementById('t'), z = document.getElementById('z'), | |
| grid = document.getElementById('grid'), | |
| channelsBox = document.getElementById('channels'), scalesBox = document.getElementById('scales'); | |
| const CHANNELS = META.channels, TVALS = META.t, LEVELS = META.levels; | |
| const SCALES = [...META.scales].reverse(); | |
| let selected = [CHANNELS[0]], scale = SCALES[0], AR = 1; | |
| function button(label) { | |
| const b = document.createElement('button'); | |
| b.type = 'button'; | |
| b.textContent = label; | |
| return b; | |
| } | |
| const chanButtons = CHANNELS.map(ch => { | |
| const b = button(ch); | |
| b.onclick = () => { | |
| if (selected.includes(ch)) { if (selected.length > 1) selected = selected.filter(x => x !== ch); } | |
| else selected.push(ch); | |
| refreshChannels(); | |
| rebuild(); | |
| }; | |
| channelsBox.appendChild(b); | |
| return b; | |
| }); | |
| function refreshChannels() { | |
| CHANNELS.forEach((ch, i) => chanButtons[i].setAttribute('aria-pressed', selected.includes(ch))); | |
| } | |
| scalesBox.setAttribute('role', 'radiogroup'); | |
| scalesBox.setAttribute('aria-label', 'colour scale'); | |
| const scaleButtons = SCALES.map((sc, i) => { | |
| const b = button(sc); | |
| b.setAttribute('role', 'radio'); | |
| b.onclick = () => selectScale(i); | |
| b.onkeydown = e => { | |
| const d = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 | |
| : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? -1 : 0; | |
| if (!d) return; | |
| e.preventDefault(); | |
| selectScale((i + d + SCALES.length) % SCALES.length); | |
| }; | |
| scalesBox.appendChild(b); | |
| return b; | |
| }); | |
| function refreshScales() { | |
| SCALES.forEach((sc, i) => { | |
| const on = sc === scale; | |
| scaleButtons[i].setAttribute('aria-checked', on); | |
| scaleButtons[i].tabIndex = on ? 0 : -1; | |
| }); | |
| } | |
| function selectScale(i) { | |
| scale = SCALES[i]; | |
| refreshScales(); | |
| scaleButtons[i].focus(); | |
| update(); | |
| } | |
| function update() { | |
| const frame = TVALS[+t.value], level = +z.value; | |
| grid.querySelectorAll('img').forEach(im => | |
| im.src = `cgi-bin/index.cgi?s=${scale}&c=${im.dataset.ch}&t=${frame}&z=${level}`); | |
| } | |
| function fit() { | |
| const n = selected.length; | |
| if (!n) return; | |
| const gap = parseFloat(getComputedStyle(grid).gap) || 0; | |
| const top = grid.getBoundingClientRect().top; | |
| const W = grid.clientWidth, H = window.innerHeight - top; | |
| let best = { cols: 1, w: 0 }; | |
| for (let cols = 1; cols <= n; cols++) { | |
| const rows = Math.ceil(n / cols); | |
| const w = Math.min((W - (cols - 1) * gap) / cols, ((H - (rows - 1) * gap) / rows) * AR); | |
| if (w > best.w) best = { cols, w }; | |
| } | |
| grid.style.gridTemplateColumns = `repeat(${best.cols}, ${Math.floor(best.w)}px)`; | |
| } | |
| function rebuild() { | |
| grid.replaceChildren(...selected.map(ch => { | |
| const im = document.createElement('img'); | |
| im.dataset.ch = ch; | |
| im.alt = ch; | |
| im.onerror = () => { im.alt = ch + ' — failed to load'; }; | |
| return im; | |
| })); | |
| const first = grid.querySelector('img'); | |
| if (first) first.onload = () => { AR = first.naturalWidth / first.naturalHeight; fit(); }; | |
| update(); | |
| fit(); | |
| } | |
| t.max = TVALS.length - 1; | |
| z.max = LEVELS.length - 1; | |
| t.addEventListener('input', update); | |
| z.addEventListener('input', update); | |
| window.addEventListener('resize', fit); | |
| let active = t; | |
| [t, z].forEach(s => s.addEventListener('focus', () => active = s)); | |
| addEventListener('keydown', e => { | |
| if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; | |
| if (document.activeElement && document.activeElement.tagName === 'BUTTON') return; | |
| e.preventDefault(); | |
| e.key === 'ArrowLeft' ? active.stepDown() : active.stepUp(); | |
| active.dispatchEvent(new Event('input')); | |
| }); | |
| refreshChannels(); | |
| refreshScales(); | |
| rebuild(); | |