Addition of increment/decrement buttons, addition of Auto Random button

Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
2025-12-16 11:19:51 +00:00
parent e6da9c8c98
commit 002fbb8b6c
10 changed files with 730 additions and 807 deletions

View File

@@ -1,263 +0,0 @@
const bitsRows = document.getElementById("bitsRows");
const denaryEl = document.getElementById("denaryNumber");
const binaryEl = document.getElementById("binaryNumber");
const modeToggle = document.getElementById("modeToggle");
const modeHint = document.getElementById("modeHint");
const bitsInput = document.getElementById("bitsInput");
const btnBitsUp = document.getElementById("btnBitsUp");
const btnBitsDown = document.getElementById("btnBitsDown");
const btnShiftLeft = document.getElementById("btnShiftLeft");
const btnShiftRight = document.getElementById("btnShiftRight");
const btnCustomBinary = document.getElementById("btnCustomBinary");
const btnCustomDenary = document.getElementById("btnCustomDenary");
let bitCount = clampInt(Number(bitsInput.value || 8), 1, 64);
let bits = new Array(bitCount).fill(false); // index 0 = MSB
function clampInt(n, min, max){
n = Number(n);
if (!Number.isFinite(n)) n = min;
n = Math.floor(n);
return Math.max(min, Math.min(max, n));
}
function isTwos(){
return !!modeToggle.checked;
}
function msbValue(){
return 2 ** (bitCount - 1);
}
function unsignedValueAt(i){
// i=0 is MSB
return 2 ** (bitCount - 1 - i);
}
function computeUnsigned(){
let sum = 0;
for (let i = 0; i < bitCount; i++){
if (bits[i]) sum += unsignedValueAt(i);
}
return sum;
}
function computeDenary(){
const u = computeUnsigned();
if (!isTwos()) return u;
// Two's complement:
// if MSB is 1, value = unsigned - 2^n
if (bits[0]) return u - (2 ** bitCount);
return u;
}
function bitsToString(){
return bits.map(b => (b ? "1" : "0")).join("");
}
function updateModeHint(){
if (isTwos()){
modeHint.textContent = "Tip: In two's complement, the left-most bit (MSB) represents a negative value.";
} else {
modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values.";
}
}
function buildBitsUI(){
bitsRows.innerHTML = "";
// Build rows of 8 bits
const rowCount = Math.ceil(bitCount / 8);
for (let r = 0; r < rowCount; r++){
const row = document.createElement("div");
row.className = "byteRow";
const start = r * 8;
const end = Math.min(start + 8, bitCount);
for (let i = start; i < end; i++){
const bitEl = document.createElement("div");
bitEl.className = "bit";
// label: show -MSB in two's complement
const labelVal = (isTwos() && i === 0) ? -msbValue() : unsignedValueAt(i);
bitEl.innerHTML = `
<span class="bulb" id="bulb-${i}" aria-hidden="true">💡</span>
<div class="bitVal">${labelVal}</div>
<label class="switch" aria-label="Toggle bit ${labelVal}">
<input type="checkbox" data-index="${i}">
<span class="slider"></span>
</label>
`;
row.appendChild(bitEl);
}
bitsRows.appendChild(row);
}
// Hook switches
bitsRows.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => {
input.addEventListener("change", () => {
const i = Number(input.dataset.index);
bits[i] = input.checked;
updateReadout();
});
});
syncUI();
}
function syncUI(){
// sync inputs
bitsRows.querySelectorAll('input[type="checkbox"][data-index]').forEach(input => {
const i = Number(input.dataset.index);
input.checked = !!bits[i];
});
// sync bulbs
for (let i = 0; i < bitCount; i++){
const bulb = document.getElementById(`bulb-${i}`);
if (bulb) bulb.classList.toggle("on", !!bits[i]);
}
updateReadout();
}
function updateReadout(){
denaryEl.textContent = String(computeDenary());
binaryEl.textContent = bitsToString();
}
function setFromBinary(bin){
const clean = String(bin).replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return false;
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
for (let i = 0; i < bitCount; i++){
bits[i] = padded[i] === "1";
}
syncUI();
return true;
}
function setFromDenary(n){
n = Number(n);
if (!Number.isInteger(n)) return false;
if (!isTwos()){
// unsigned: 0 .. (2^n - 1)
const max = (2 ** bitCount) - 1;
if (n < 0 || n > max) return false;
for (let i = 0; i < bitCount; i++){
const v = unsignedValueAt(i);
if (n >= v){
bits[i] = true;
n -= v;
} else {
bits[i] = false;
}
}
syncUI();
return true;
}
// two's complement: -(2^(n-1)) .. (2^(n-1)-1)
const min = -(2 ** (bitCount - 1));
const max = (2 ** (bitCount - 1)) - 1;
if (n < min || n > max) return false;
// convert to unsigned representation
let u = n;
if (u < 0) u = (2 ** bitCount) + u; // wrap
for (let i = 0; i < bitCount; i++){
const v = unsignedValueAt(i);
if (u >= v){
bits[i] = true;
u -= v;
} else {
bits[i] = false;
}
}
syncUI();
return true;
}
function shiftLeft(){
bits.shift(); // drop MSB
bits.push(false); // add LSB
syncUI();
}
function shiftRight(){
bits.pop(); // drop LSB
bits.unshift(false); // add MSB
syncUI();
}
function setBitCount(newCount){
newCount = clampInt(newCount, 4, 64);
if (newCount === bitCount) return;
// preserve right-most bits (LSB side) when resizing
const old = bits.slice();
const next = new Array(newCount).fill(false);
const copy = Math.min(old.length, next.length);
for (let k = 0; k < copy; k++){
// copy from end (LSB)
next[newCount - 1 - k] = old[old.length - 1 - k];
}
bitCount = newCount;
bits = next;
bitsInput.value = String(bitCount);
buildBitsUI();
}
/* -------------------- events -------------------- */
modeToggle.addEventListener("change", () => {
updateModeHint();
buildBitsUI(); // rebuild labels (MSB becomes negative/positive)
});
btnBitsUp.addEventListener("click", () => setBitCount(bitCount + 1));
btnBitsDown.addEventListener("click", () => setBitCount(bitCount - 1));
bitsInput.addEventListener("change", () => setBitCount(Number(bitsInput.value)));
btnShiftLeft.addEventListener("click", shiftLeft);
btnShiftRight.addEventListener("click", shiftRight);
btnCustomBinary.addEventListener("click", () => {
const v = prompt(`Enter ${bitCount}-bit binary:`);
if (v === null) return;
if (!setFromBinary(v)) alert("Invalid binary. Use only 0 and 1.");
});
btnCustomDenary.addEventListener("click", () => {
const min = isTwos() ? -(2 ** (bitCount - 1)) : 0;
const max = isTwos() ? (2 ** (bitCount - 1)) - 1 : (2 ** bitCount) - 1;
const v = prompt(`Enter a denary number (${min} to ${max}):`);
if (v === null) return;
if (!setFromDenary(Number(v))) alert("Invalid denary for current mode/bit width.");
});
/* -------------------- init -------------------- */
bitsInput.value = String(bitCount);
updateModeHint();
buildBitsUI();

View File

@@ -1,284 +0,0 @@
/* Font */
@font-face{
font-family: "DSEG7ClassicRegular";
src:
url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
.binaryWrap{
padding-top: 8px;
}
.topGrid{
display:grid;
grid-template-columns: 1fr 340px;
gap: 28px;
align-items:start;
}
.readout{
text-align:center;
padding: 10px 10px 0;
}
.label{
letter-spacing: .18em;
font-weight: 800;
color: var(--muted);
text-transform: uppercase;
font-size: 13px;
margin-top: 10px;
}
.num{
font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 400;
color: var(--accent);
text-shadow: 0 0 18px var(--accent-dim);
}
.denary{
font-size: 70px; /* smaller than before */
line-height: 1;
margin: 6px 0 10px;
}
.binary{
font-size: 54px; /* smaller than before */
letter-spacing: .12em;
line-height: 1;
margin: 6px 0 16px;
}
.controls{
margin-top: 10px;
display:flex;
gap: 12px;
justify-content:center;
flex-wrap:wrap;
}
.btn{
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.14);
color: #fff;
padding: 12px 14px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
min-width: 160px;
}
.btn:active{ transform: translateY(1px); }
.divider{
margin-top: 26px;
border-top: 1px solid var(--line);
}
/* Right-side cards */
.panelCol{
display:flex;
flex-direction:column;
gap: 14px;
}
.card{
background: var(--panel2);
border: 1px solid rgba(255,255,255,.10);
border-radius: 14px;
padding: 14px;
}
.cardTitle{
letter-spacing: .18em;
font-weight: 900;
color: var(--muted);
text-transform: uppercase;
font-size: 12px;
margin: 0 0 10px;
}
.hint{
color: var(--muted);
font-size: 12px;
margin-top: 8px;
line-height: 1.35;
}
.toggleRow{
display:flex;
align-items:center;
justify-content:space-between;
gap: 10px;
}
.toggleLabel{
color: var(--text);
font-weight: 800;
font-size: 14px;
}
/* Switch (reused for mode + bits) */
.switch{
position: relative;
width: 56px;
height: 34px;
display:inline-block;
flex: 0 0 auto;
}
.switch input{
position:absolute;
opacity:0;
width:0;
height:0;
}
.slider{
position:absolute;
inset:0;
background: rgba(255,255,255,.10);
border: 1px solid rgba(255,255,255,.14);
border-radius: 999px;
transition: .18s ease;
}
.slider::before{
content:"";
position:absolute;
height: 28px;
width: 28px;
left: 3px;
top: 2px;
background: rgba(255,255,255,.92);
border-radius: 50%;
transition: .18s ease;
}
.switch input:checked + .slider{
background: rgba(51,255,122,.20);
border-color: rgba(51,255,122,.55);
}
.switch input:checked + .slider::before{
transform: translateX(22px);
background: var(--accent);
}
/* Bit-width control */
.bitWidthRow{
display:grid;
grid-template-columns: 44px 1fr 44px;
gap: 10px;
align-items:center;
}
.miniBtn{
height: 44px;
width: 44px;
border-radius: 12px;
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.14);
color: #fff;
cursor: pointer;
font-weight: 900;
font-size: 18px;
}
.bitInputWrap{
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.14);
border-radius: 12px;
padding: 10px 12px;
display:flex;
align-items:center;
justify-content:space-between;
gap: 12px;
}
.bitInputLabel{
color: var(--muted);
font-size: 12px;
font-weight: 900;
letter-spacing: .18em;
text-transform: uppercase;
}
.bitInput{
width: 86px;
text-align:right;
background: transparent;
border:none;
outline:none;
color: var(--accent);
font-family: "DSEG7ClassicRegular", ui-monospace, monospace;
font-size: 28px;
}
.bitInput::-webkit-outer-spin-button,
.bitInput::-webkit-inner-spin-button{
-webkit-appearance:none;
margin:0;
}
/* Bits: wrap every 8 (rows built in JS) */
.bitsRows{
margin-top: 22px;
display:flex;
flex-direction:column;
gap: 18px;
}
/* A row of up to 8 bits */
.byteRow{
display:flex;
justify-content:center;
gap: 18px;
flex-wrap:nowrap;
}
/* A single bit */
.bit{
width: 110px;
display:flex;
flex-direction:column;
align-items:center;
gap: 10px;
padding: 6px 4px;
}
/* Bulb emoji: bigger */
.bulb{
font-size: 30px;
line-height: 1;
opacity: .20;
filter: grayscale(1);
transform: translateY(2px);
}
.bulb.on{
opacity: 1;
filter: grayscale(0);
text-shadow: 0 0 16px rgba(255,216,107,.65);
}
.bitVal{
font-family: "DSEG7ClassicRegular", ui-monospace, monospace;
font-size: 30px;
color: var(--text);
opacity: .92;
min-height: 34px;
}
/* Responsiveness */
@media (max-width: 980px){
.topGrid{ grid-template-columns: 1fr; }
.denary{ font-size: 62px; }
.binary{ font-size: 48px; }
.bit{ width: 96px; }
}
@media (max-width: 520px){
.denary{ font-size: 56px; }
.binary{ font-size: 42px; }
.btn{ min-width: 140px; }
.byteRow{ gap: 12px; }
.bit{ width: 86px; }
}

View File

@@ -1,78 +0,0 @@
:root{
--bg: #1f2027;
--panel: rgba(255,255,255,.04);
--text: #e8e8ee;
--muted: #a9acb8;
--line: rgba(255,255,255,.12);
--accent: #33ff7a;
--accent-dim: rgba(51,255,122,.15);
}
*{ 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);
}
.page{
min-height: calc(100vh - 120px);
}
.siteHeader{
position: sticky;
top: 0;
z-index: 10;
background: rgba(0,0,0,.15);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(10px);
}
.siteHeader__inner{
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.brand{
color: var(--text);
text-decoration: none;
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: 14px;
}
.nav__link:hover{ color: var(--text); }
.siteFooter{
border-top: 1px solid var(--line);
background: rgba(0,0,0,.10);
}
.siteFooter__inner{
max-width: 1200px;
margin: 0 auto;
padding: 18px 20px;
color: var(--muted);
font-size: 12px;
display: grid;
gap: 6px;
}