Fix broken code

Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
2025-12-16 11:30:59 +00:00
parent 002fbb8b6c
commit ac585701a3
5 changed files with 509 additions and 525 deletions

View File

@@ -1,7 +1,4 @@
--- ---
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
const { title = "Computing:Box" } = Astro.props; const { title = "Computing:Box" } = Astro.props;
--- ---
@@ -11,25 +8,34 @@ const { title = "Computing:Box" } = Astro.props;
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{title}</title> <title>{title}</title>
<link rel="stylesheet" href="/src/styles/site.css" />
</head> </head>
<body> <body>
<Header /> <header class="site-header">
<main class="page"> <div class="site-header__inner">
<div class="brand">Computing:Box</div>
<nav class="nav">
<a class="nav__link" href="/binary">Binary</a>
<a class="nav__link" href="/hexadecimal">Hexadecimal</a>
<a class="nav__link" href="/hex-colours">Hex Colours</a>
<a class="nav__link" href="/logic-gates">Logic Gates</a>
<a class="nav__link" href="/about">About</a>
</nav>
</div>
</header>
<main class="site-main">
<slot /> <slot />
</main> </main>
<Footer />
<footer class="site-footer">
<div class="site-footer__inner">
<div>Computer Science Concept Simulators</div>
<div>© 2025 Computing:Box · Created with 💜 by Mr Lyall</div>
<div>Powered by ADCM Networks</div>
</div>
</footer>
</body> </body>
</html> </html>
<style>
:global(html, body) {
height: 100%;
}
:global(body) {
margin: 0;
}
.page {
min-height: calc(100vh - 64px - 120px); /* header + footer-ish */
}
</style>

View File

@@ -1,43 +1,30 @@
--- ---
import BaseLayout from "../layouts/BaseLayout.astro";
import "../styles/binary.css"; import "../styles/binary.css";
// keeps JS in src/ and lets Vite/Astro bundle it properly // ✅ Correct Astro v5 way: bundle script from src/ and get its final URL
const scriptUrl = Astro.resolve("../scripts/binary.js"); import binaryScriptUrl from "../scripts/binary.js?url";
--- ---
<!doctype html> <BaseLayout title="Binary | Computing:Box">
<html lang="en"> <section class="binary-wrap">
<head> <div class="topGrid">
<meta charset="utf-8" /> <!-- LEFT: readout + main buttons -->
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Binary | Computing:Box</title>
<script type="module" src={scriptUrl}></script>
</head>
<body>
<!-- Your site header/footer are already present in your layout/screenshots.
If this page is wrapped by a global Layout, leave it there.
If not, keep your existing header/footer includes here. -->
<main class="wrap">
<section class="topGrid">
<!-- LEFT: readout + buttons -->
<div> <div>
<div class="readout"> <div class="readout">
<div class="label">Denary</div> <div class="label">Denary</div>
<div id="denaryNumber" class="num denaryValue">0</div> <div id="denaryNumber" class="num denary">0</div>
<div class="label">Binary</div> <div class="label">Binary</div>
<div id="binaryNumber" class="num binaryValue">0</div> <pre id="binaryNumber" class="num binary" aria-label="Binary value">0</pre>
<!-- Orange box: custom on one row, shift on next row --> <!-- ORANGE: custom + shifts on separate lines -->
<div class="controls"> <div class="controls controls--twoRows">
<div class="controlRow"> <div class="controlsRow">
<button class="btn btnPrimary" id="btnCustomBinary" type="button">Custom Binary</button> <button class="btn btn--green" id="btnCustomBinary" type="button">Custom Binary</button>
<button class="btn btnPrimary" id="btnCustomDenary" type="button">Custom Denary</button> <button class="btn btn--green" id="btnCustomDenary" type="button">Custom Denary</button>
</div> </div>
<div class="controlRow"> <div class="controlsRow">
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button> <button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
<button class="btn" id="btnShiftRight" type="button">Right Shift</button> <button class="btn" id="btnShiftRight" type="button">Right Shift</button>
</div> </div>
@@ -50,26 +37,8 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
<section class="bits" id="bitsGrid" aria-label="Bit switches"></section> <section class="bits" id="bitsGrid" aria-label="Bit switches"></section>
</div> </div>
<!-- RIGHT: actions + mode + bit width --> <!-- RIGHT: red buttons go above/below this panel (not in the middle) -->
<aside class="panelCol"> <aside class="panelCol">
<!-- Red box: move these buttons to the panel -->
<div class="card">
<div class="cardTitle">Actions</div>
<div class="actionGrid">
<button class="btn" id="btnClear" type="button">Clear</button>
<button class="btn btnSpin" id="btnDec1" type="button" aria-label="Decrease by 1">1</button>
<button class="btn btnSpin" id="btnInc1" type="button" aria-label="Increase by 1">+1</button>
<button class="btn" id="btnAutoRandom" type="button">Auto Random</button>
</div>
<div class="hint">
1/+1 steps the current value by one. Auto Random runs briefly then stops automatically.
</div>
</div>
<div class="card"> <div class="card">
<div class="cardTitle">Mode</div> <div class="cardTitle">Mode</div>
@@ -81,7 +50,7 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
<span class="slider"></span> <span class="slider"></span>
</label> </label>
<div class="toggleLabel" id="lblTwos">Twos complement</div> <div class="toggleLabel" id="lblTwos">Two&apos;s complement</div>
</div> </div>
<div class="hint" id="modeHint"> <div class="hint" id="modeHint">
@@ -89,6 +58,20 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
</div> </div>
</div> </div>
<!-- RED: extra buttons ABOVE bit width -->
<div class="card">
<div class="cardTitle">Tools</div>
<div class="toolsGrid">
<button class="btn" id="btnClear" type="button">Clear</button>
<button class="btn btn--spin" id="btnMinus1" type="button">1</button>
<button class="btn btn--spin" id="btnPlus1" type="button">+1</button>
<button class="btn" id="btnAutoRandom" type="button">Auto Random</button>
</div>
<div class="hint">
Auto Random runs briefly then stops automatically.
</div>
</div>
<div class="card"> <div class="card">
<div class="cardTitle">Bit width</div> <div class="cardTitle">Bit width</div>
@@ -116,7 +99,8 @@ const scriptUrl = Astro.resolve("../scripts/binary.js");
<div class="hint">Minimum 1 bit, maximum 64 bits.</div> <div class="hint">Minimum 1 bit, maximum 64 bits.</div>
</div> </div>
</aside> </aside>
</div>
<script type="module" src={binaryScriptUrl}></script>
</section> </section>
</main> </BaseLayout>
</body>
</html>

View File

@@ -1,10 +1,3 @@
/* Binary simulator (Unsigned + Two's complement)
- bits 1..64
- wrap bits every 8 visually (CSS), no scrollbars
- bulbs always update in BOTH modes
- MSB label shows negative weight in two's complement (e.g. -128)
*/
const bitsGrid = document.getElementById("bitsGrid"); const bitsGrid = document.getElementById("bitsGrid");
const denaryEl = document.getElementById("denaryNumber"); const denaryEl = document.getElementById("denaryNumber");
const binaryEl = document.getElementById("binaryNumber"); const binaryEl = document.getElementById("binaryNumber");
@@ -22,122 +15,57 @@ const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnCustomDenary = document.getElementById("btnCustomDenary"); const btnCustomDenary = document.getElementById("btnCustomDenary");
const btnClear = document.getElementById("btnClear"); const btnClear = document.getElementById("btnClear");
const btnDec1 = document.getElementById("btnDec1"); const btnMinus1 = document.getElementById("btnMinus1");
const btnInc1 = document.getElementById("btnInc1"); const btnPlus1 = document.getElementById("btnPlus1");
const btnAutoRandom = document.getElementById("btnAutoRandom"); const btnAutoRandom = document.getElementById("btnAutoRandom");
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64); let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
let isTwos = Boolean(modeToggle?.checked);
// state is MSB -> LSB let bits = new Array(bitCount).fill(false); // MSB at index 0
let bits = new Array(bitCount).fill(false);
let autoTimer = null; let autoTimer = null;
function clampInt(n, min, max){ function clampInt(n, min, max){
n = Number(n); n = Number(n);
if (!Number.isFinite(n)) return min; if (!Number.isFinite(n)) return min;
n = Math.floor(n); n = Math.trunc(n);
if (n < min) return min; return Math.max(min, Math.min(max, n));
if (n > max) return max;
return n;
} }
function pow2Big(n){ /* ----------------------------
// n is number (0..63) Label values (MSB..LSB)
return 1n << BigInt(n); Unsigned: [2^(n-1) ... 1]
} Two's: [-2^(n-1), 2^(n-2) ... 1]
----------------------------- */
function isTwos(){ function getLabelValues(){
return !!modeToggle?.checked; const vals = [];
}
function getRange(){
// returns { min: BigInt, max: BigInt, mod: BigInt }
const n = bitCount;
const mod = pow2Big(n);
if (!isTwos()){
return { min: 0n, max: mod - 1n, mod };
}
// two's: [-2^(n-1), 2^(n-1)-1]
if (n === 1){
return { min: -1n, max: 0n, mod };
}
const half = pow2Big(n - 1);
return { min: -half, max: half - 1n, mod };
}
function currentValueBig(){
// interpret current bits as unsigned or two's complement signed
let unsigned = 0n;
for (let i = 0; i < bitCount; i++){ for (let i = 0; i < bitCount; i++){
if (!bits[i]) continue; const pow = bitCount - 1 - i;
const shift = BigInt(bitCount - 1 - i); let v = 2 ** pow;
unsigned += 1n << shift; if (isTwos && i === 0) v = -v; // ✅ MSB label becomes negative
vals.push(v);
} }
return vals;
if (!isTwos()){
return unsigned;
}
// signed decode
const signBit = bits[0];
if (!signBit) return unsigned;
const { mod } = getRange();
return unsigned - mod; // two's complement negative
}
function setFromUnsignedBig(u){
// u in [0, 2^n-1]
const n = bitCount;
for (let i = 0; i < n; i++){
const shift = BigInt(n - 1 - i);
bits[i] = ((u >> shift) & 1n) === 1n;
}
}
function setFromValueBig(v){
// v is signed depending on mode. We convert to bit pattern.
const { min, max, mod } = getRange();
if (v < min) v = min;
if (v > max) v = max;
if (!isTwos()){
setFromUnsignedBig(v);
return;
}
// two's: if negative, add 2^n
let u = v;
if (u < 0n) u = u + mod;
setFromUnsignedBig(u);
} }
function buildBits(){ function buildBits(){
// wrap every 8 bits
bitsGrid.style.setProperty("--cols", String(Math.min(8, bitCount)));
bitsGrid.innerHTML = ""; bitsGrid.innerHTML = "";
const labelValues = getLabelValues();
for (let i = 0; i < bitCount; i++){ for (let i = 0; i < bitCount; i++){
const placePow = bitCount - 1 - i;
const unsignedWeight = pow2Big(placePow);
// label weight depends on mode (MSB negative in two's)
let label = unsignedWeight.toString();
if (isTwos() && i === 0){
label = "-" + unsignedWeight.toString();
}
const bit = document.createElement("div"); const bit = document.createElement("div");
bit.className = "bit"; bit.className = "bit";
bit.innerHTML = ` bit.innerHTML = `
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div> <div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
<div class="bitVal">${label}</div> <div class="bitVal num" id="label-${i}">${labelValues[i]}</div>
<label class="switch" aria-label="Toggle bit ${label}"> <label class="switch" aria-label="Toggle bit">
<input type="checkbox" data-index="${i}"> <input type="checkbox" data-index="${i}">
<span class="slider"></span> <span class="slider"></span>
</label> </label>
`; `;
bitsGrid.appendChild(bit); bitsGrid.appendChild(bit);
} }
@@ -146,134 +74,175 @@ function buildBits(){
input.addEventListener("change", () => { input.addEventListener("change", () => {
const idx = Number(input.dataset.index); const idx = Number(input.dataset.index);
bits[idx] = input.checked; bits[idx] = input.checked;
updateReadout(); updateUI();
}); });
}); });
syncUI(); updateUI();
} }
function binaryStringGrouped(){ function setLabels(){
const raw = bits.map(b => (b ? "1" : "0")).join(""); const labelValues = getLabelValues();
// group every 8 from the RIGHT (LSB side) so long widths look sane for (let i = 0; i < bitCount; i++){
// Example: 11 bits -> 00000000 000 (as in your screenshot) const el = document.getElementById(`label-${i}`);
const groups = []; if (el) el.textContent = String(labelValues[i]);
for (let end = raw.length; end > 0; end -= 8){
const start = Math.max(0, end - 8);
groups.unshift(raw.slice(start, end));
} }
return groups.join(" ");
} }
function updateModeHint(){ function bitsToUnsigned(){
if (!modeHint) return; let n = 0;
if (isTwos()){ for (let i = 0; i < bitCount; i++){
if (!bits[i]) continue;
const pow = bitCount - 1 - i;
n += 2 ** pow;
}
return n;
}
function bitsToTwos(){
// Two's complement interpretation
// value = -MSB*2^(n-1) + sum(other set bits)
let n = 0;
for (let i = 0; i < bitCount; i++){
if (!bits[i]) continue;
const pow = bitCount - 1 - i;
const v = 2 ** pow;
if (i === 0) n -= v;
else n += v;
}
return n;
}
function getCurrentValue(){
return isTwos ? bitsToTwos() : bitsToUnsigned();
}
function setFromUnsignedValue(n){
// clamp to range of bitCount
const max = (2 ** bitCount) - 1;
n = clampInt(n, 0, max);
for (let i = 0; i < bitCount; i++){
const pow = bitCount - 1 - i;
const v = 2 ** pow;
if (n >= v){
bits[i] = true;
n -= v;
} else {
bits[i] = false;
}
}
syncSwitchesAndBulbs();
updateUI(false);
}
function setFromTwosValue(n){
// represent in two's complement with bitCount bits:
// allowed range: [-2^(n-1), 2^(n-1)-1]
const min = -(2 ** (bitCount - 1));
const max = (2 ** (bitCount - 1)) - 1;
n = clampInt(n, min, max);
// Convert to unsigned representation modulo 2^bitCount
const mod = 2 ** bitCount;
let u = ((n % mod) + mod) % mod;
// then set bits from unsigned u
for (let i = 0; i < bitCount; i++){
const pow = bitCount - 1 - i;
const v = 2 ** pow;
if (u >= v){
bits[i] = true;
u -= v;
} else {
bits[i] = false;
}
}
syncSwitchesAndBulbs();
updateUI(false);
}
function formatBinary(groupsOf = 4){
const raw = bits.map(b => (b ? "1" : "0")).join("");
// group for readability (keeps your “wrap every 8 bits” layout for switches;
// this just formats the readout)
let out = "";
for (let i = 0; i < raw.length; i++){
out += raw[i];
const isLast = i === raw.length - 1;
if (!isLast && (i + 1) % groupsOf === 0) out += " ";
}
return out.trim();
}
function syncSwitchesAndBulbs(){
// ✅ Bulbs always update (unsigned OR two's)
bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => {
const idx = Number(input.dataset.index);
input.checked = Boolean(bits[idx]);
});
for (let i = 0; i < bitCount; i++){
const bulb = document.getElementById(`bulb-${i}`);
if (bulb) bulb.classList.toggle("on", Boolean(bits[i]));
}
}
function updateUI(sync = true){
if (sync) syncSwitchesAndBulbs();
// labels update when mode changes
setLabels();
// readouts
const value = getCurrentValue();
denaryEl.textContent = String(value);
binaryEl.textContent = formatBinary(4);
// hint
if (isTwos){
modeHint.textContent = "Tip: In twos complement, the left-most bit (MSB) represents a negative value."; modeHint.textContent = "Tip: In twos complement, the left-most bit (MSB) represents a negative value.";
} else { } else {
modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values.";
} }
} }
function updateReadout(){ /* ----------------------------
const v = currentValueBig(); Controls
----------------------------- */
// display btnShiftLeft?.addEventListener("click", () => {
denaryEl.textContent = v.toString(); // shift left: drop MSB, append 0 to LSB
binaryEl.textContent = binaryStringGrouped();
// bulbs update ALWAYS (mode should not affect bulb on/off)
for (let i = 0; i < bitCount; i++){
const bulb = document.getElementById(`bulb-${i}`);
if (bulb) bulb.classList.toggle("on", bits[i]);
}
}
function syncUI(){
// sync switch positions
bitsGrid.querySelectorAll('input[type="checkbox"][data-index]').forEach((input) => {
const idx = Number(input.dataset.index);
input.checked = !!bits[idx];
});
updateReadout();
}
function clearBits(){
bits = new Array(bitCount).fill(false);
syncUI();
}
function shiftLeft(){
// logical left shift: drop MSB, add 0 at LSB
bits.shift(); bits.shift();
bits.push(false); bits.push(false);
syncUI(); updateUI();
} });
function shiftRight(){ btnShiftRight?.addEventListener("click", () => {
// logical right shift: drop LSB, add 0 at MSB // shift right: drop LSB, insert 0 at MSB
bits.pop(); bits.pop();
bits.unshift(false); bits.unshift(false);
syncUI(); updateUI();
} });
function setFromBinaryPrompt(){ btnClear?.addEventListener("click", () => {
const v = prompt(`Enter binary (${bitCount} bits). Spaces allowed:`); bits = new Array(bitCount).fill(false);
if (v === null) return; updateUI();
});
const clean = String(v).replace(/\s+/g, ""); btnMinus1?.addEventListener("click", () => {
if (!/^[01]+$/.test(clean)){ const v = getCurrentValue();
alert("Invalid input. Use only 0 and 1 (spaces allowed)."); if (isTwos) setFromTwosValue(v - 1);
return; else setFromUnsignedValue(v - 1);
} });
const padded = clean.slice(-bitCount).padStart(bitCount, "0"); btnPlus1?.addEventListener("click", () => {
bits = [...padded].map(ch => ch === "1"); const v = getCurrentValue();
syncUI(); if (isTwos) setFromTwosValue(v + 1);
} else setFromUnsignedValue(v + 1);
});
function setFromDenaryPrompt(){ btnAutoRandom?.addEventListener("click", () => {
const v = prompt(`Enter denary (${isTwos() ? "signed" : "unsigned"}).`); // stop if already running
if (v === null) return;
// BigInt parse (handles negatives)
let n;
try {
n = BigInt(String(v).trim());
} catch {
alert("Invalid number.");
return;
}
setFromValueBig(n);
syncUI();
}
function stepBy(delta){
const v = currentValueBig();
const next = v + BigInt(delta);
// wrap within valid range
const { min, max, mod } = getRange();
let wrapped = next;
if (!isTwos()){
// unsigned wrap: modulo 2^n
wrapped = ((next % mod) + mod) % mod;
} else {
// signed wrap across [min..max]
const span = max - min + 1n; // equals 2^n
wrapped = next;
while (wrapped > max) wrapped -= span;
while (wrapped < min) wrapped += span;
}
setFromValueBig(wrapped);
syncUI();
}
function autoRandomOnce(){
// runs briefly then stops automatically
if (autoTimer){ if (autoTimer){
clearInterval(autoTimer); clearInterval(autoTimer);
autoTimer = null; autoTimer = null;
@@ -281,110 +250,98 @@ function autoRandomOnce(){
return; return;
} }
btnAutoRandom.textContent = "Auto Random (Running…)"; btnAutoRandom.textContent = "Stop Random";
const { min, max, mod } = getRange();
// run briefly then stop automatically
const start = Date.now(); const start = Date.now();
const durationMs = 1800; // short burst const durationMs = 2200; // auto stop
const tickMs = 90;
autoTimer = setInterval(() => { autoTimer = setInterval(() => {
const now = Date.now(); const now = Date.now();
if (now - start >= durationMs){ if (now - start > durationMs){
clearInterval(autoTimer); clearInterval(autoTimer);
autoTimer = null; autoTimer = null;
btnAutoRandom.textContent = "Auto Random"; btnAutoRandom.textContent = "Auto Random";
return; return;
} }
// pick a random unsigned pattern 0..2^n-1 then interpret via mode // random within correct range for current mode
// (this keeps distribution consistent even for signed mode) if (isTwos){
const r = randomBigIntBelow(mod); const min = -(2 ** (bitCount - 1));
setFromUnsignedBig(r); const max = (2 ** (bitCount - 1)) - 1;
syncUI(); const n = Math.floor(Math.random() * (max - min + 1)) + min;
}, tickMs); setFromTwosValue(n);
} } else {
const max = (2 ** bitCount) - 1;
const n = Math.floor(Math.random() * (max + 1));
setFromUnsignedValue(n);
}
}, 90);
});
function randomBigIntBelow(maxExclusive){ btnCustomBinary?.addEventListener("click", () => {
// maxExclusive up to 2^64 const v = prompt(`Enter a ${bitCount}-bit binary number (0/1):`);
// Use crypto if available, otherwise fallback (still fine for teaching tool) if (v === null) return;
const n = bitCount;
if (globalThis.crypto && crypto.getRandomValues){ const clean = v.replace(/\s+/g, "");
const bytes = Math.ceil(n / 8); if (!/^[01]+$/.test(clean)){
const buf = new Uint8Array(bytes); alert("Invalid binary. Use only 0 and 1.");
return;
while (true){
crypto.getRandomValues(buf);
let val = 0n;
for (const b of buf){
val = (val << 8n) + BigInt(b);
} }
// mask extra bits const padded = clean.slice(-bitCount).padStart(bitCount, "0");
const extra = BigInt(bytes * 8 - n); bits = [...padded].map(ch => ch === "1");
if (extra > 0n) val = val & ((1n << BigInt(n)) - 1n); updateUI();
});
if (val < maxExclusive) return val; btnCustomDenary?.addEventListener("click", () => {
} const v = prompt(isTwos
? `Enter a denary number (${-(2 ** (bitCount - 1))} to ${(2 ** (bitCount - 1)) - 1}):`
: `Enter a denary number (0 to ${(2 ** bitCount) - 1}):`
);
if (v === null) return;
const n = Number(v);
if (!Number.isFinite(n) || !Number.isInteger(n)){
alert("Invalid denary. Enter a whole number.");
return;
} }
// fallback if (isTwos) setFromTwosValue(n);
const maxNum = Number.MAX_SAFE_INTEGER; else setFromUnsignedValue(n);
let val = BigInt(Math.floor(Math.random() * maxNum)); });
return val % maxExclusive;
}
function setBitCount(nextCount){ /* ----------------------------
nextCount = clampInt(nextCount, 1, 64); Mode + Bit width
bitCount = nextCount; ----------------------------- */
modeToggle?.addEventListener("change", () => {
isTwos = Boolean(modeToggle.checked);
// keep the same underlying bit pattern; just reinterpret and relabel
updateUI(false);
});
btnBitsUp?.addEventListener("click", () => {
bitCount = clampInt(bitCount + 1, 1, 64);
bitsInput.value = String(bitCount); bitsInput.value = String(bitCount);
// preserve current value if possible by re-encoding it into new width
const v = currentValueBig();
bits = new Array(bitCount).fill(false); bits = new Array(bitCount).fill(false);
setFromValueBig(v);
buildBits(); buildBits();
updateModeHint(); });
}
function onModeChange(){
updateModeHint();
// rebuild labels so MSB shows negative weight in two's mode
// preserve current *bit pattern* (not numeric), because students are toggling interpretation
const currentPattern = bits.slice();
btnBitsDown?.addEventListener("click", () => {
bitCount = clampInt(bitCount - 1, 1, 64);
bitsInput.value = String(bitCount);
bits = new Array(bitCount).fill(false);
buildBits(); buildBits();
bits = currentPattern.slice(0, bitCount); });
// if length changed (shouldn't), pad bitsInput?.addEventListener("change", () => {
if (bits.length < bitCount){ bitCount = clampInt(Number(bitsInput.value), 1, 64);
bits = bits.concat(new Array(bitCount - bits.length).fill(false)); bitsInput.value = String(bitCount);
} bits = new Array(bitCount).fill(false);
buildBits();
});
// rebuild labels again (already done), then resync /* ----------------------------
syncUI(); Init
} ----------------------------- */
/* ----------------- Hooks ----------------- */
btnShiftLeft?.addEventListener("click", shiftLeft);
btnShiftRight?.addEventListener("click", shiftRight);
btnCustomBinary?.addEventListener("click", setFromBinaryPrompt);
btnCustomDenary?.addEventListener("click", setFromDenaryPrompt);
btnClear?.addEventListener("click", clearBits);
btnDec1?.addEventListener("click", () => stepBy(-1));
btnInc1?.addEventListener("click", () => stepBy(+1));
btnAutoRandom?.addEventListener("click", autoRandomOnce);
btnBitsUp?.addEventListener("click", () => setBitCount(bitCount + 1));
btnBitsDown?.addEventListener("click", () => setBitCount(bitCount - 1));
bitsInput?.addEventListener("change", () => setBitCount(Number(bitsInput.value)));
modeToggle?.addEventListener("change", onModeChange);
/* ----------------- Init ----------------- */
updateModeHint();
buildBits(); buildBits();

View File

@@ -1,38 +1,16 @@
:root{ /* DSEG7ClassicRegular font */
--bg: #1f2027;
--panel: #22242d;
--panel2: rgba(255,255,255,.04);
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
}
@font-face{ @font-face{
font-family: "DSEG7ClassicRegular"; font-family: "DSEG7ClassicRegular";
src: src:
url("/fonts/DSEG7Classic-Regular.woff2") format("woff2"), url("/fonts/DSEG7Classic-Regular.ttf") format("truetype"),
url("/fonts/DSEG7Classic-Regular.woff") format("woff"), url("/fonts/DSEG7Classic-Regular.woff") format("woff");
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
body{ .binary-wrap{
margin:0; padding-top: 6px;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.wrap{
max-width: 1200px;
margin: 0 auto;
padding: 32px 20px 60px;
} }
.topGrid{ .topGrid{
@@ -64,32 +42,41 @@ body{
text-shadow: 0 0 18px var(--accent-dim); text-shadow: 0 0 18px var(--accent-dim);
} }
.denaryValue{ .denary{
font-size: 72px; font-size: 72px; /* smaller */
line-height: 1.0; line-height: 1.0;
margin: 6px 0 10px; margin: 6px 0 10px;
} }
.binaryValue{ .binary{
font-size: 52px; font-size: 46px; /* smaller */
letter-spacing: .10em; letter-spacing: .12em;
line-height: 1.0; line-height: 1.15;
margin: 6px 0 14px; margin: 6px 0 14px;
white-space: pre-wrap;
word-break: break-word;
display:inline-block;
text-align:center;
} }
.controls{ .controls{
margin-top: 10px; margin-top: 8px;
display:flex; display:flex;
flex-direction:column; justify-content:center;
gap: 10px; gap: 12px;
align-items:center; flex-wrap: wrap;
} }
.controlRow{ .controls--twoRows{
flex-direction: column;
gap: 10px;
}
.controlsRow{
display:flex; display:flex;
gap: 12px; gap: 12px;
justify-content:center; justify-content:center;
flex-wrap:nowrap; flex-wrap: wrap;
} }
.btn{ .btn{
@@ -102,34 +89,37 @@ body{
cursor: pointer; cursor: pointer;
min-width: 160px; min-width: 160px;
} }
.btn:active{ transform: translateY(1px); } .btn:active{ transform: translateY(1px); }
.btnPrimary{ .btn--green{
background: rgba(51,255,122,.18); background: rgba(51,255,122,.16);
border-color: rgba(51,255,122,.45); border-color: rgba(51,255,122,.45);
box-shadow: 0 0 0 1px rgba(51,255,122,.10) inset;
} }
.btnSpin{ .btn--green:hover{
background: rgba(51,255,122,.22);
}
.btn--spin{
min-width: 120px; min-width: 120px;
font-size: 18px; font-size: 18px; /* bigger */
padding: 12px 10px;
} }
.divider{ .divider{
margin-top: 26px; margin-top: 22px;
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
.panelCol{ .panelCol{
display:flex; display:flex;
flex-direction:column; flex-direction:column;
gap:14px; gap: 14px;
} }
.card{ .card{
background: var(--panel2); background: var(--panel);
border: 1px solid rgba(255,255,255,.10); border: 1px solid var(--panel-border);
border-radius: 14px; border-radius: 14px;
padding: 14px; padding: 14px;
} }
@@ -154,7 +144,7 @@ body{
display:flex; display:flex;
align-items:center; align-items:center;
justify-content:space-between; justify-content:space-between;
gap:10px; gap: 10px;
} }
.toggleLabel{ .toggleLabel{
@@ -164,11 +154,11 @@ body{
} }
.switch{ .switch{
position: relative; position:relative;
width: 56px; width:56px;
height: 34px; height:34px;
display:inline-block; display:inline-block;
flex: 0 0 auto; flex:0 0 auto;
} }
.switch input{ .switch input{
opacity:0; opacity:0;
@@ -180,19 +170,19 @@ body{
inset:0; inset:0;
background: rgba(255,255,255,.10); background: rgba(255,255,255,.10);
border: 1px solid rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14);
border-radius: 999px; border-radius:999px;
transition: .18s ease; transition:.18s ease;
} }
.slider::before{ .slider::before{
content:""; content:"";
position:absolute; position:absolute;
height: 28px; height:28px;
width: 28px; width:28px;
left: 3px; left:3px;
top: 2px; top:2px;
background: rgba(255,255,255,.92); background: rgba(255,255,255,.92);
border-radius: 50%; border-radius:50%;
transition: .18s ease; transition:.18s ease;
} }
.switch input:checked + .slider{ .switch input:checked + .slider{
background: rgba(51,255,122,.20); background: rgba(51,255,122,.20);
@@ -205,8 +195,8 @@ body{
.bitWidthRow{ .bitWidthRow{
display:grid; display:grid;
grid-template-columns: 44px 1fr 44px; grid-template-columns:44px 1fr 44px;
gap: 10px; gap:10px;
align-items:center; align-items:center;
} }
@@ -218,69 +208,56 @@ body{
border: 1px solid rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14);
color:#fff; color:#fff;
cursor:pointer; cursor:pointer;
font-weight: 900; font-weight:900;
font-size: 18px; font-size:18px;
} }
.bitInputWrap{ .bitInputWrap{
background: rgba(255,255,255,.06); background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.14);
border-radius: 12px; border-radius:12px;
padding: 10px 12px; padding:10px 12px;
display:flex; display:flex;
align-items:center; align-items:center;
justify-content:space-between; justify-content:space-between;
gap: 12px; gap:12px;
} }
.bitInputLabel{ .bitInputLabel{
color: var(--muted); color: var(--muted);
font-size: 12px; font-size:12px;
font-weight: 900; font-weight:900;
letter-spacing: .18em; letter-spacing:.18em;
text-transform: uppercase; text-transform:uppercase;
} }
.bitInput{ .bitInput{
width: 86px; width:86px;
text-align:right; text-align:right;
background: transparent; background: transparent;
border:none; border: none;
outline:none; outline: none;
color: var(--accent); color: var(--accent);
font-family: "DSEG7ClassicRegular", ui-monospace, monospace; font-family:"DSEG7ClassicRegular", ui-monospace, monospace;
font-size: 28px; font-size:28px;
} }
.bitInput::-webkit-outer-spin-button, .bitInput::-webkit-outer-spin-button,
.bitInput::-webkit-inner-spin-button{ .bitInput::-webkit-inner-spin-button{
-webkit-appearance: none; -webkit-appearance:none;
margin: 0; margin:0;
} }
.actionGrid{ /* Bits: wrap every 8 */
display:grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.actionGrid .btn{
min-width: 0;
width: 100%;
}
/* -------- Bits grid: wrap every 8, centered when <8, no scrollbar -------- */
.bits{ .bits{
margin-top: 22px; --cols: 8;
margin-top: 26px;
padding-top: 18px; padding-top: 18px;
display:grid;
display: grid; grid-template-columns: repeat(var(--cols), 90px);
grid-template-columns: repeat(8, 92px); justify-content: center; /* centres when < 8 too */
gap: 18px; gap: 18px;
justify-content: center; align-items:end;
text-align:center;
width: fit-content;
max-width: 100%;
margin-left: auto;
margin-right: auto;
} }
.bit{ .bit{
@@ -291,45 +268,30 @@ body{
padding: 8px 4px; padding: 8px 4px;
} }
/* Bulb: emoji 💡 larger, grey when off, glowing when on */ /* Bulb emoji bigger */
.bulb{ .bulb{
font-size: 28px; /* bigger bulb */ font-size: 26px; /* bigger */
line-height: 1; line-height: 1;
filter: grayscale(100%) brightness(.85); filter: grayscale(1);
opacity: .65; opacity: .35;
transform: translateY(2px); transform: translateY(2px);
user-select: none;
} }
.bulb.on{ .bulb.on{
filter: none; filter: grayscale(0);
opacity: 1; opacity: 1;
text-shadow: 0 0 18px rgba(255, 216, 107, .55); text-shadow: 0 0 16px rgba(255,216,107,.55);
} }
/* Bit place value */
.bitVal{ .bitVal{
font-family: "DSEG7ClassicRegular", ui-monospace, monospace; font-size: 28px;
font-size: 30px;
color: var(--text); color: var(--text);
opacity: .95; opacity: .95;
line-height: 1; line-height: 1;
min-height: 34px; min-height: 34px;
} }
/* Per-bit switch */
.bit .switch{
transform: scale(1.05);
}
@media (max-width: 980px){ @media (max-width: 980px){
.topGrid{ grid-template-columns: 1fr; } .topGrid{ grid-template-columns: 1fr; }
.denaryValue{ font-size: 64px; } .denary{ font-size: 62px; }
.binaryValue{ font-size: 46px; } .binary{ font-size: 40px; }
.btn{ min-width: 140px; }
}
@media (max-width: 520px){
.bits{
grid-template-columns: repeat(4, 92px);
}
} }

75
src/styles/site.css Normal file
View File

@@ -0,0 +1,75 @@
:root{
--bg: #1f2027;
--panel: rgba(255,255,255,.04);
--panel-border: rgba(255,255,255,.10);
--text: #e8e8ee;
--muted: #a9acb8;
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
--line: rgba(255,255,255,.12);
}
*{ box-sizing: border-box; }
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.site-header{
border-bottom: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.12);
}
.site-header__inner{
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display:flex;
align-items:center;
justify-content:space-between;
gap: 18px;
}
.brand{
font-weight: 800;
letter-spacing: .02em;
}
.nav{
display:flex;
gap: 14px;
flex-wrap: wrap;
justify-content:flex-end;
}
.nav__link{
color: var(--muted);
text-decoration:none;
font-weight: 700;
font-size: 13px;
}
.nav__link:hover{ color: var(--text); }
.site-main{
max-width: 1200px;
margin: 0 auto;
padding: 28px 20px 40px;
min-height: calc(100vh - 140px);
}
.site-footer{
border-top: 1px solid rgba(255,255,255,.08);
background: rgba(0,0,0,.10);
}
.site-footer__inner{
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}