Files
computing-box/src/scripts/hexadecimal.js
Alexander Lyall ffab71cfcc
All checks were successful
Pre-release on non-main branches / prerelease (push) Successful in 27s
Completed Wave 3 features:
- [X] New User Interface (Responsive)
- [X] Two's Compliment Simulator
- [X] Extended Binary Simulator (Custom bit sizes)
- [X] Unified Binary Simulator (Unsigned & Two's Completment combined)
- [X] Extended Hexadecimal Simulator
- [X] Unified Hexadecimal Simulator (For GCSE & A Level Specification)
- [X] Enhanced Gate Simulator (Truth Table Creator)
- [X] Compound Gate Simulator
- [ ] Computer Components Simulator

Signed-off-by: Alexander Lyall <alex@adcm.uk>
2026-03-01 16:22:58 +00:00

312 lines
9.4 KiB
JavaScript

// src/scripts/hexadecimal.js
// Computing:Box — Hexadecimal page logic
(() => {
/* -----------------------------
DOM
----------------------------- */
const hexGrid = document.getElementById("hexGrid");
const denaryEl = document.getElementById("denaryNumber");
const binaryEl = document.getElementById("binaryNumber");
const hexEl = document.getElementById("hexNumber");
const digitsInput = document.getElementById("digitsInput");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnCustomHex = document.getElementById("btnCustomHex");
const btnDec = document.getElementById("btnDec");
const btnInc = document.getElementById("btnInc");
const btnClear = document.getElementById("btnClear");
const btnRandom = document.getElementById("btnRandom");
const btnDigitsUp = document.getElementById("btnDigitsUp");
const btnDigitsDown = document.getElementById("btnDigitsDown");
const toolboxToggle = document.getElementById("toolboxToggle");
const hexPage = document.getElementById("hexPage");
/* -----------------------------
STATE
----------------------------- */
let hexCount = clampInt(Number(digitsInput?.value ?? 2), 1, 16);
let nibbles = new Array(hexCount).fill(0);
let randomTimer = null;
/* -----------------------------
HELPERS
----------------------------- */
function clampInt(n, min, max) {
if (!Number.isFinite(n)) return min;
return Math.max(min, Math.min(max, Math.trunc(n)));
}
function maxExclusive() {
return 1n << BigInt(hexCount * 4);
}
function maxValue() {
return maxExclusive() - 1n;
}
function getValue() {
let v = 0n;
for (let i = 0; i < hexCount; i++) {
v += BigInt(nibbles[i]) << BigInt(i * 4);
}
return v;
}
function setValue(v) {
if (v < 0n) return false;
if (v > maxValue()) return false;
for (let i = 0; i < hexCount; i++) {
nibbles[i] = Number((v >> BigInt(i * 4)) & 0xFn);
}
return true;
}
/* -----------------------------
RESPONSIVE GRID
----------------------------- */
function computeColsForHexGrid() {
if (!hexGrid) return;
hexGrid.style.setProperty("--cols", String(Math.min(hexCount, 8)));
hexGrid.classList.toggle("bitsFew", hexCount < 4);
}
/* -----------------------------
BUILD UI (CARDS + BULBS)
----------------------------- */
function buildGrid(count) {
hexCount = clampInt(count, 1, 16);
if (digitsInput) digitsInput.value = String(hexCount);
const oldNibbles = nibbles.slice();
nibbles = new Array(hexCount).fill(0);
for (let i = 0; i < Math.min(oldNibbles.length, hexCount); i++) {
nibbles[i] = oldNibbles[i];
}
hexGrid.innerHTML = "";
for (let i = hexCount - 1; i >= 0; i--) {
const col = document.createElement("div");
col.className = "hexCol";
let cardHTML = `
<div class="hexCard">
<div class="hexCardButtons">
<button class="hexCardBtn inc" id="hexInc-${i}">▲</button>
<button class="hexCardBtn dec" id="hexDec-${i}">▼</button>
</div>
<div class="hexDigitDisplay num" id="hexDisplay-${i}">0</div>
<div class="hexNibbleRow">
`;
for(let j = 3; j >= 0; j--) {
cardHTML += `
<div class="hexNibbleBit">
<div class="bulb hexNibbleBulb" id="hexBulb-${i}-${j}" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.25a6.75 6.75 0 0 0-6.75 6.75c0 2.537 1.393 4.75 3.493 5.922l.507.282v1.546h5.5v-1.546l.507-.282A6.75 6.75 0 0 0 12 2.25Zm-2.25 16.5v.75a2.25 2.25 0 0 0 4.5 0v-.75h-4.5Z"/>
</svg>
</div>
<div class="hexNibbleLabel">${1 << j}</div>
</div>
`;
}
cardHTML += `
</div>
</div>
<div class="hexColWeight">${(1n << BigInt(i * 4)).toString()}</div>
`;
col.innerHTML = cardHTML;
const incBtn = col.querySelector(`#hexInc-${i}`);
const decBtn = col.querySelector(`#hexDec-${i}`);
incBtn.addEventListener("click", () => {
const span = maxExclusive();
const weight = 1n << BigInt(i * 4);
setValue((getValue() + weight) % span);
updateUI();
});
decBtn.addEventListener("click", () => {
const span = maxExclusive();
const weight = 1n << BigInt(i * 4);
setValue((getValue() - weight + span) % span);
updateUI();
});
hexGrid.appendChild(col);
}
computeColsForHexGrid();
updateUI();
}
/* -----------------------------
UI UPDATE
----------------------------- */
function updateUI() {
const val = getValue();
if (denaryEl) denaryEl.textContent = val.toString();
if (hexEl) hexEl.textContent = val.toString(16).toUpperCase().padStart(hexCount, '0');
if (binaryEl) {
let binStr = "";
for (let i = hexCount - 1; i >= 0; i--) {
binStr += nibbles[i].toString(2).padStart(4, '0') + " ";
}
binaryEl.textContent = binStr.trimEnd();
}
for (let i = 0; i < hexCount; i++) {
const display = document.getElementById(`hexDisplay-${i}`);
if (display) display.textContent = nibbles[i].toString(16).toUpperCase();
for (let j = 0; j < 4; j++) {
const bulb = document.getElementById(`hexBulb-${i}-${j}`);
if (bulb) {
const isOn = (nibbles[i] & (1 << j)) !== 0;
bulb.classList.toggle("on", isOn);
}
}
}
}
/* -----------------------------
CLEAR / INC / DEC
----------------------------- */
function clearAll() {
nibbles.fill(0);
buildGrid(2);
}
function increment() {
const span = maxExclusive();
setValue((getValue() + 1n) % span);
updateUI();
}
function decrement() {
const span = maxExclusive();
setValue((getValue() - 1n + span) % span);
updateUI();
}
/* -----------------------------
RANDOM
----------------------------- */
function cryptoRandomBigInt(maxExcl) {
if (maxExcl <= 0n) return 0n;
const bitLen = maxExcl.toString(2).length;
const byteLen = Math.ceil(bitLen / 8);
while (true) {
const bytes = new Uint8Array(byteLen);
crypto.getRandomValues(bytes);
let x = 0n;
for (const b of bytes) x = (x << 8n) | BigInt(b);
const extraBits = BigInt(byteLen * 8 - bitLen);
if (extraBits > 0n) x = x >> extraBits;
if (x < maxExcl) return x;
}
}
function setRandomOnce() {
const u = cryptoRandomBigInt(maxExclusive());
setValue(u);
updateUI();
}
function runRandomBriefly() {
if (randomTimer) {
clearInterval(randomTimer);
randomTimer = null;
}
if (btnRandom) btnRandom.classList.add("btnRandomRunning");
const start = Date.now();
const durationMs = 1125;
const tickMs = 80;
randomTimer = setInterval(() => {
setRandomOnce();
if (Date.now() - start >= durationMs) {
clearInterval(randomTimer);
randomTimer = null;
if (btnRandom) btnRandom.classList.remove("btnRandomRunning");
}
}, tickMs);
}
/* -----------------------------
EVENTS
----------------------------- */
btnCustomHex?.addEventListener("click", () => {
const v = prompt(`Enter hexadecimal (0-9, A-F). Current width: ${hexCount} digits`);
if (v === null) return;
const clean = v.replace(/\s+/g, "").toUpperCase();
if (!/^[0-9A-F]+$/.test(clean)) return alert("Invalid hexadecimal.");
if (clean.length > hexCount) return alert("Value too large for current digit width.");
if (!setValue(BigInt("0x" + clean))) alert("Value out of range.");
else updateUI();
});
btnCustomBinary?.addEventListener("click", () => {
const v = prompt(`Enter binary (0, 1). Current width: ${hexCount * 4} bits`);
if (v === null) return;
const clean = v.replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return alert("Invalid binary.");
if (clean.length > hexCount * 4) return alert("Value too large for current digit width.");
if (!setValue(BigInt("0b" + clean))) alert("Value out of range.");
else updateUI();
});
btnCustomDenary?.addEventListener("click", () => {
const v = prompt(`Enter denary (0 to ${maxValue().toString()}):`);
if (v === null) return;
const clean = v.trim();
if (!/^\d+$/.test(clean)) return alert("Invalid denary. Digits only.");
if (!setValue(BigInt(clean))) alert(`Value out of range. Enter a number between 0 and ${maxValue().toString()}.`);
else updateUI();
});
btnInc?.addEventListener("click", increment);
btnDec?.addEventListener("click", decrement);
btnClear?.addEventListener("click", clearAll);
btnRandom?.addEventListener("click", runRandomBriefly);
btnDigitsUp?.addEventListener("click", () => buildGrid(hexCount + 1));
btnDigitsDown?.addEventListener("click", () => buildGrid(hexCount - 1));
digitsInput?.addEventListener("change", () => buildGrid(Number(digitsInput.value)));
toolboxToggle?.addEventListener("click", () => {
const isCollapsed = hexPage?.classList.contains("toolboxCollapsed");
hexPage.classList.toggle("toolboxCollapsed", !isCollapsed);
toolboxToggle?.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
});
window.addEventListener("resize", computeColsForHexGrid);
/* -----------------------------
INIT
----------------------------- */
buildGrid(hexCount);
})();