refactor: isolate Radio controls and tuner presentation

This commit is contained in:
Sameh Khamis committed 2026-09-12 23:23:20 -07:00
1 parent 79a0f09087
commit 7b9aca8597
21 files changed
+1507 -1134

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
/** Map an integer tuner slot directly to one available directory station. */
export function radioTunerSlot(value, stationCount) {
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
if (!count) return { slot: 0, max: 0, locked: false, stationIndex: -1, leftIndex: -1, rightIndex: -1 };
const max = Math.max(0, count - 1);
const slot = Math.min(max, Math.max(0, Math.round(Number(value) || 0)));
return {
slot,
max,
locked: true,
stationIndex: slot,
leftIndex: slot,
rightIndex: slot,
};
}
/** Snap a tuner release to the nearest available directory station. */
export function radioTunerCommitSlot(value, stationCount) {
return radioTunerSlot(value, stationCount);
}
/** Map one pointer coordinate to continuous absolute directory progress. */
export function radioTunerPointerPosition(clientX, left, width, stationCount, insetPx = 7) {
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
if (!count) return { ratio: 0, coordinate: 0, stationIndex: -1 };
if (count === 1) return { ratio: 0.5, coordinate: 0, stationIndex: 0 };
const inset = Math.max(0, Number(insetPx) || 0);
const usableWidth = Math.max(1, (Number(width) || 0) - inset * 2);
const ratio = Math.min(1, Math.max(0, ((Number(clientX) || 0) - (Number(left) || 0) - inset) / usableWidth));
const coordinate = ratio * (count - 1);
return {
ratio,
coordinate,
stationIndex: Math.min(count - 1, Math.max(0, Math.floor(coordinate + 0.5))),
};
}
/** Build a bounded virtual tuner tape around one continuous directory coordinate. */
export function buildRadioTunerTicks(coordinate, stationCount, width, {
insetPx = 7,
minPitchPx = 14,
speedFactor = 5,
overscan = 2,
labelStep = 6,
} = {}) {
const count = Math.max(0, Math.floor(Number(stationCount) || 0));
const dialWidth = Math.max(0, Number(width) || 0);
const inset = Math.max(0, Number(insetPx) || 0);
const usableWidth = Math.max(0, dialWidth - inset * 2);
if (!count) return { ticks: [], needleX: inset, pitchPx: 0, ratio: 0 };
const value = Math.min(count - 1, Math.max(0, Number(coordinate) || 0));
if (count === 1) {
return {
ticks: [{ stationIndex: 0, channel: 1, xPx: inset + usableWidth / 2, current: true, label: '01' }],
needleX: inset + usableWidth / 2,
pitchPx: Math.max(1, Number(minPitchPx) || 14),
ratio: 0.5,
};
}
const directoryStep = usableWidth / (count - 1);
const pitchPx = Math.max(
Math.max(1, Number(minPitchPx) || 14),
directoryStep * Math.max(1, Number(speedFactor) || 5),
);
const needleX = inset + directoryStep * value;
const overscanPx = pitchPx * Math.max(0, Number(overscan) || 0);
const first = Math.max(0, Math.ceil(value + (-overscanPx - needleX) / pitchPx));
const last = Math.min(count - 1, Math.floor(value + (dialWidth + overscanPx - needleX) / pitchPx));
const currentIndex = Math.min(count - 1, Math.max(0, Math.floor(value + 0.5)));
const majorEvery = Math.max(1, Math.floor(Number(labelStep) || 6));
const labelWidth = Math.max(2, String(count).length);
const ticks = [];
for (let stationIndex = first; stationIndex <= last; stationIndex += 1) {
const channel = stationIndex + 1;
const current = stationIndex === currentIndex;
const labelled = current || stationIndex === 0 || stationIndex === count - 1 || channel % majorEvery === 0;
ticks.push({
stationIndex,
channel,
xPx: needleX + pitchPx * (stationIndex - value),
current,
label: labelled ? String(channel).padStart(labelWidth, '0') : '',
});
}
return { ticks, needleX, pitchPx, ratio: value / (count - 1) };
}