You've already forked computing-box
Binary simulator is feature complete.
Known issues: - Formatting of the navbar across the site is broken - Formatting of the Binary simulator is broken Signed-off-by: Alexander Lyall <alex@adcm.uk>
This commit is contained in:
104
src/components/simulators/HexSimulator.astro
Normal file
104
src/components/simulators/HexSimulator.astro
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
import "./hex/hex-simulator.css";
|
||||
---
|
||||
|
||||
<section class="hex-sim" data-hex-sim>
|
||||
<div class="hex-main">
|
||||
<div class="hex-readout">
|
||||
<div class="hex-label">DENARY</div>
|
||||
<div class="hex-number" data-out="denary">0</div>
|
||||
|
||||
<div class="hex-label hex-mt">HEXADECIMAL</div>
|
||||
<div class="hex-number hex-number--small" data-out="hex">00</div>
|
||||
|
||||
<div class="hex-label hex-mt">BINARY</div>
|
||||
<div class="hex-number hex-number--tiny" data-out="bin">0000 0000</div>
|
||||
</div>
|
||||
|
||||
<div class="hex-divider"></div>
|
||||
|
||||
<div class="hex-digits" data-out="digitsRow"></div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbox button -->
|
||||
<button class="hex-toolbox-btn" type="button" data-action="toggleToolbox" aria-controls="hex-toolbox" aria-expanded="true">
|
||||
<span class="hex-toolbox-icon" aria-hidden="true">
|
||||
<!-- toolbox icon -->
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none">
|
||||
<path d="M9 7V6a3 3 0 0 1 6 0v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M4 9h16l-1.3 10.4A2 2 0 0 1 16.7 21H7.3a2 2 0 0 1-1.98-1.6L4 9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M10 13h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
TOOLBOX
|
||||
</button>
|
||||
|
||||
<!-- Toolbox panel -->
|
||||
<aside class="hex-toolbox is-open" id="hex-toolbox" data-out="toolbox">
|
||||
<div class="hex-panel">
|
||||
<div class="hex-panel-title">SETTINGS</div>
|
||||
|
||||
<div class="hex-setting-title">HEX DIGIT WIDTH</div>
|
||||
|
||||
<div class="hex-width">
|
||||
<button class="hex-btn hex-btn--square" type="button" data-action="digitsMinus">−</button>
|
||||
|
||||
<div class="hex-width-readout">
|
||||
<div class="hex-width-label">DIGITS</div>
|
||||
<div class="hex-width-number" data-out="digitsCount">2</div>
|
||||
</div>
|
||||
|
||||
<button class="hex-btn hex-btn--square" type="button" data-action="digitsPlus">+</button>
|
||||
</div>
|
||||
|
||||
<div class="hex-hint" data-out="bitsHint">= 8 bits</div>
|
||||
</div>
|
||||
|
||||
<div class="hex-panel">
|
||||
<div class="hex-panel-title">CUSTOM NUMBER</div>
|
||||
|
||||
<div class="hex-grid-2">
|
||||
<button class="hex-btn hex-btn--green" type="button" data-action="customHex">Custom Hexadecimal</button>
|
||||
<button class="hex-btn hex-btn--green" type="button" data-action="customDenary">Custom Denary</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom Binary + Random on SAME row, same size -->
|
||||
<div class="hex-grid-2 hex-mt-sm">
|
||||
<button class="hex-btn hex-btn--green" type="button" data-action="customBinary">Custom Binary</button>
|
||||
<button class="hex-btn hex-btn--wide hex-btn--random" type="button" data-action="random" data-random>Random</button>
|
||||
</div>
|
||||
|
||||
<div class="hex-tiny-note">RANDOM RUNS BRIEFLY THEN STOPS AUTOMATICALLY.</div>
|
||||
</div>
|
||||
|
||||
<div class="hex-panel">
|
||||
<div class="hex-panel-title">TOOLS</div>
|
||||
|
||||
<div class="hex-tools-top">
|
||||
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="decrement" title="Decrement">▼</button>
|
||||
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="increment" title="Increment">▲</button>
|
||||
</div>
|
||||
|
||||
<button class="hex-btn hex-btn--wide hex-btn--reset" type="button" data-action="reset">Reset</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Custom number dialog -->
|
||||
<dialog class="hex-dialog" data-out="dialog">
|
||||
<div class="hex-dialog-card">
|
||||
<div class="hex-dialog-title" data-out="dialogTitle">Custom</div>
|
||||
|
||||
<input class="hex-dialog-input hex-font-mono" data-out="dialogInput" />
|
||||
|
||||
<div class="hex-dialog-hint" data-out="dialogHint"></div>
|
||||
<div class="hex-dialog-error" data-out="dialogError" aria-live="polite"></div>
|
||||
|
||||
<div class="hex-dialog-actions">
|
||||
<button class="hex-btn" type="button" data-action="dialogCancel">Cancel</button>
|
||||
<button class="hex-btn hex-btn--green" type="button" data-action="dialogApply">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/src/components/simulators/hex/hex-simulator.ts"></script>
|
||||
</section>
|
||||
346
src/components/simulators/hex/hex-simulator.css
Normal file
346
src/components/simulators/hex/hex-simulator.css
Normal file
@@ -0,0 +1,346 @@
|
||||
/* ================= Fonts to match Binary ================= */
|
||||
/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */
|
||||
@font-face {
|
||||
font-family: "DSEG7Classic";
|
||||
src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
|
||||
url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "SevenSegment";
|
||||
src: url("/fonts/Seven-Segment.woff2") format("woff2"),
|
||||
url("/fonts/Seven-Segment.woff") format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.hex-sim {
|
||||
min-height: 100vh;
|
||||
background: #14151c;
|
||||
color: #e7e8ee;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; }
|
||||
.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; }
|
||||
|
||||
.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; }
|
||||
|
||||
.hex-readout { text-align: center; }
|
||||
.hex-label {
|
||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.hex-mt { margin-top: 12px; }
|
||||
|
||||
.hex-number {
|
||||
font-family: "DSEG7Classic", ui-monospace, monospace;
|
||||
font-size: 76px;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
color: #46ff8a;
|
||||
text-shadow: 0 0 18px rgba(70,255,138,0.18);
|
||||
}
|
||||
.hex-number--small { font-size: 64px; }
|
||||
.hex-number--tiny { font-size: 54px; letter-spacing: 6px; }
|
||||
|
||||
.hex-divider {
|
||||
margin: 26px auto 18px;
|
||||
height: 1px;
|
||||
width: min(760px, 90%);
|
||||
background: rgba(255,255,255,0.10);
|
||||
}
|
||||
|
||||
/* ================= Main digit columns ================= */
|
||||
.hex-digits {
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hex-digit-col {
|
||||
width: 160px;
|
||||
border-radius: 18px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.hex-digit-controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hex-digit-char {
|
||||
font-size: 64px;
|
||||
line-height: 1;
|
||||
color: #46ff8a;
|
||||
text-shadow: 0 0 18px rgba(70,255,138,0.18);
|
||||
}
|
||||
|
||||
.hex-digit-place {
|
||||
font-family: "SevenSegment", ui-monospace, monospace;
|
||||
opacity: 0.65;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* ================= Bulbs (brightness changes) ================= */
|
||||
.hex-bulbs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.hex-bulb {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
opacity: 0.35;
|
||||
filter: grayscale(30%);
|
||||
transition: opacity 160ms ease, filter 160ms ease;
|
||||
}
|
||||
|
||||
.hex-bulb .hex-bulb-cap {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.22);
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
}
|
||||
|
||||
.hex-bulb .hex-bulb-glow {
|
||||
width: 18px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(70,255,138,0.0);
|
||||
box-shadow: 0 0 0 rgba(70,255,138,0.0);
|
||||
transition: background 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.hex-bulb .hex-bulb-label {
|
||||
font-family: "SevenSegment", ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hex-bulb.is-on {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
.hex-bulb.is-on .hex-bulb-cap {
|
||||
background: rgba(255,255,255,0.35);
|
||||
}
|
||||
.hex-bulb.is-on .hex-bulb-glow {
|
||||
background: rgba(70,255,138,0.25);
|
||||
box-shadow: 0 0 18px rgba(70,255,138,0.35);
|
||||
}
|
||||
|
||||
/* ================= Buttons (toolbox style reused everywhere) ================= */
|
||||
.hex-btn {
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: #e7e8ee;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
}
|
||||
.hex-btn:hover { background: rgba(255,255,255,0.10); }
|
||||
|
||||
.hex-btn--square {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.hex-btn--wide { width: 100%; }
|
||||
|
||||
.hex-btn--green {
|
||||
background: rgba(46, 200, 120, 0.18);
|
||||
border-color: rgba(46,200,120,0.35);
|
||||
}
|
||||
.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); }
|
||||
|
||||
.hex-btn--green2 {
|
||||
background: rgba(46, 200, 120, 0.18);
|
||||
border-color: rgba(46,200,120,0.35);
|
||||
}
|
||||
|
||||
.hex-btn--red {
|
||||
background: rgba(220, 60, 70, 0.18);
|
||||
border-color: rgba(220,60,70,0.35);
|
||||
}
|
||||
|
||||
/* Random = green pulse while running */
|
||||
.hex-btn--random.is-running {
|
||||
border-color: rgba(80, 255, 160, 0.55);
|
||||
background: rgba(46, 200, 120, 0.22);
|
||||
box-shadow: 0 0 18px rgba(80, 255, 160, 0.35);
|
||||
animation: hexPulseGreen 900ms ease-in-out infinite;
|
||||
}
|
||||
@keyframes hexPulseGreen {
|
||||
0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); }
|
||||
50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); }
|
||||
}
|
||||
|
||||
/* Reset = red background + pulse on hover */
|
||||
.hex-btn--reset:hover {
|
||||
background: rgba(220, 60, 70, 0.28);
|
||||
border-color: rgba(255, 80, 90, 0.55);
|
||||
animation: hexPulseRed 900ms ease-in-out infinite;
|
||||
}
|
||||
@keyframes hexPulseRed {
|
||||
0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); }
|
||||
50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); }
|
||||
}
|
||||
|
||||
/* ================= Toolbox button + panel (slide) ================= */
|
||||
.hex-toolbox-btn {
|
||||
position: fixed;
|
||||
top: 88px;
|
||||
right: 28px;
|
||||
z-index: 30;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: #e7e8ee;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hex-toolbox-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #ff4fa6;
|
||||
filter: drop-shadow(0 0 10px rgba(255,79,166,0.35));
|
||||
}
|
||||
|
||||
.hex-toolbox {
|
||||
position: fixed;
|
||||
top: 140px;
|
||||
right: 28px;
|
||||
width: 340px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
z-index: 25;
|
||||
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
transition: transform 220ms ease, opacity 220ms ease;
|
||||
}
|
||||
.hex-toolbox:not(.is-open) {
|
||||
transform: translateX(380px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hex-panel {
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
padding: 14px;
|
||||
}
|
||||
.hex-panel-title {
|
||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; }
|
||||
|
||||
.hex-width {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 48px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.hex-width-readout {
|
||||
border-radius: 14px;
|
||||
background: rgba(0,0,0,0.22);
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
.hex-width-label {
|
||||
font-family: "SevenSegment", ui-sans-serif, system-ui;
|
||||
opacity: 0.7;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; }
|
||||
|
||||
.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
||||
|
||||
.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.hex-mt-sm { margin-top: 10px; }
|
||||
|
||||
.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }
|
||||
.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
||||
|
||||
/* ================= Dialog ================= */
|
||||
.hex-dialog { border: none; padding: 0; background: transparent; }
|
||||
.hex-dialog::backdrop { background: rgba(0,0,0,0.55); }
|
||||
|
||||
.hex-dialog-card {
|
||||
width: min(560px, 92vw);
|
||||
border-radius: 18px;
|
||||
background: #1a1b24;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
padding: 16px;
|
||||
color: #e7e8ee;
|
||||
}
|
||||
|
||||
.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
||||
|
||||
.hex-dialog-input {
|
||||
width: 100%;
|
||||
padding: 12px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
background: rgba(0,0,0,0.25);
|
||||
color: #e7e8ee;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
||||
.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
|
||||
.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hex-toolbox { width: min(360px, 92vw); right: 16px; }
|
||||
.hex-toolbox-btn { right: 16px; }
|
||||
.hex-number { font-size: 60px; }
|
||||
.hex-number--tiny { font-size: 40px; letter-spacing: 4px; }
|
||||
}
|
||||
232
src/components/simulators/hex/hex-simulator.ts
Normal file
232
src/components/simulators/hex/hex-simulator.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
type DialogMode = "hex" | "den" | "bin";
|
||||
|
||||
const root = document.querySelector<HTMLElement>("[data-hex-sim]");
|
||||
if (!root) throw new Error("Hex simulator root not found");
|
||||
|
||||
const outDen = root.querySelector<HTMLElement>('[data-out="denary"]')!;
|
||||
const outHex = root.querySelector<HTMLElement>('[data-out="hex"]')!;
|
||||
const outBin = root.querySelector<HTMLElement>('[data-out="bin"]')!;
|
||||
const outDigitsRow = root.querySelector<HTMLElement>('[data-out="digitsRow"]')!;
|
||||
|
||||
const toolbox = root.querySelector<HTMLElement>('[data-out="toolbox"]')!;
|
||||
const toolboxBtn = root.querySelector<HTMLButtonElement>('[data-action="toggleToolbox"]')!;
|
||||
const digitsCount = root.querySelector<HTMLElement>('[data-out="digitsCount"]')!;
|
||||
const bitsHint = root.querySelector<HTMLElement>('[data-out="bitsHint"]')!;
|
||||
const randomBtn = root.querySelector<HTMLButtonElement>("[data-random]")!;
|
||||
|
||||
const dialog = root.querySelector<HTMLDialogElement>('[data-out="dialog"]')!;
|
||||
const dialogTitle = root.querySelector<HTMLElement>('[data-out="dialogTitle"]')!;
|
||||
const dialogInput = root.querySelector<HTMLInputElement>('[data-out="dialogInput"]')!;
|
||||
const dialogHint = root.querySelector<HTMLElement>('[data-out="dialogHint"]')!;
|
||||
const dialogError = root.querySelector<HTMLElement>('[data-out="dialogError"]')!;
|
||||
|
||||
let digits = 2; // 1..8
|
||||
let value = 0; // unsigned denary
|
||||
let randomTimer: number | null = null;
|
||||
let dialogMode: DialogMode | null = null;
|
||||
|
||||
const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
|
||||
const maxForDigits = (d: number) => (16 ** d) - 1;
|
||||
|
||||
const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0");
|
||||
const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0");
|
||||
const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim();
|
||||
|
||||
function stopRandom(): void {
|
||||
if (randomTimer !== null) window.clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
randomBtn.classList.remove("is-running");
|
||||
}
|
||||
|
||||
function startRandom(): void {
|
||||
stopRandom();
|
||||
const max = maxForDigits(digits);
|
||||
const start = Date.now();
|
||||
|
||||
randomBtn.classList.add("is-running");
|
||||
|
||||
randomTimer = window.setInterval(() => {
|
||||
value = Math.floor(Math.random() * (max + 1));
|
||||
render();
|
||||
if (Date.now() - start > 1600) stopRandom();
|
||||
}, 90);
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
const bits = digits * 4;
|
||||
|
||||
digitsCount.textContent = String(digits);
|
||||
bitsHint.textContent = `= ${bits} bits`;
|
||||
|
||||
outDen.textContent = String(value);
|
||||
outHex.textContent = padHex(value, digits);
|
||||
outBin.textContent = groupBin(padBin(value, bits));
|
||||
|
||||
renderDigitsRow();
|
||||
}
|
||||
|
||||
function renderDigitsRow(): void {
|
||||
const hex = padHex(value, digits);
|
||||
outDigitsRow.innerHTML = "";
|
||||
|
||||
for (let i = 0; i < digits; i++) {
|
||||
const pow = digits - 1 - i;
|
||||
const placeValue = 16 ** pow;
|
||||
|
||||
const digitChar = hex[i];
|
||||
const digitVal = parseInt(digitChar, 16);
|
||||
const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1
|
||||
|
||||
const col = document.createElement("div");
|
||||
col.className = "hex-digit-col";
|
||||
col.innerHTML = `
|
||||
<div class="hex-digit-controls">
|
||||
<button class="hex-btn hex-btn--square hex-btn--green2" type="button" data-action="digitUp" data-i="${i}" title="Increase">▲</button>
|
||||
<button class="hex-btn hex-btn--square hex-btn--red" type="button" data-action="digitDown" data-i="${i}" title="Decrease">▼</button>
|
||||
</div>
|
||||
|
||||
<div class="hex-digit-char hex-font-number">${digitChar}</div>
|
||||
|
||||
<!-- bulbs: brightness changes based on nibble bits -->
|
||||
<div class="hex-bulbs" aria-label="Nibble bits">
|
||||
${[8,4,2,1].map((w, idx) => {
|
||||
const on = nibbleBits[idx] === 1;
|
||||
return `
|
||||
<div class="hex-bulb ${on ? "is-on" : ""}">
|
||||
<div class="hex-bulb-cap"></div>
|
||||
<div class="hex-bulb-glow"></div>
|
||||
<div class="hex-bulb-label">${w}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("")}
|
||||
</div>
|
||||
|
||||
<div class="hex-digit-place">${placeValue}</div>
|
||||
`;
|
||||
outDigitsRow.appendChild(col);
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(mode: DialogMode): void {
|
||||
stopRandom();
|
||||
dialogMode = mode;
|
||||
|
||||
dialogError.textContent = "";
|
||||
dialogInput.value = "";
|
||||
|
||||
if (mode === "hex") {
|
||||
dialogTitle.textContent = "Custom Hexadecimal";
|
||||
dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`;
|
||||
dialogInput.placeholder = "A1";
|
||||
dialogInput.inputMode = "text";
|
||||
} else if (mode === "den") {
|
||||
dialogTitle.textContent = "Custom Denary";
|
||||
dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
|
||||
dialogInput.placeholder = "42";
|
||||
dialogInput.inputMode = "numeric";
|
||||
} else {
|
||||
dialogTitle.textContent = "Custom Binary";
|
||||
dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
|
||||
dialogInput.placeholder = "00101010";
|
||||
dialogInput.inputMode = "text";
|
||||
}
|
||||
|
||||
dialog.showModal();
|
||||
window.setTimeout(() => dialogInput.focus(), 0);
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
dialogMode = null;
|
||||
dialogError.textContent = "";
|
||||
if (dialog.open) dialog.close();
|
||||
}
|
||||
|
||||
function applyDialog(): void {
|
||||
const raw = (dialogInput.value || "").trim();
|
||||
if (!dialogMode) return closeDialog();
|
||||
if (raw.length === 0) return closeDialog();
|
||||
|
||||
const max = maxForDigits(digits);
|
||||
const bits = digits * 4;
|
||||
|
||||
if (dialogMode === "hex") {
|
||||
const v = raw.toUpperCase();
|
||||
if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; }
|
||||
if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
|
||||
value = clamp(parseInt(v, 16), 0, max);
|
||||
render();
|
||||
return closeDialog();
|
||||
}
|
||||
|
||||
if (dialogMode === "den") {
|
||||
if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
|
||||
value = clamp(n, 0, max);
|
||||
render();
|
||||
return closeDialog();
|
||||
}
|
||||
|
||||
// bin
|
||||
if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
|
||||
if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
|
||||
value = clamp(parseInt(raw, 2), 0, max);
|
||||
render();
|
||||
return closeDialog();
|
||||
}
|
||||
|
||||
function applyDigitDelta(i: number, delta: number): void {
|
||||
stopRandom();
|
||||
const hexArr = padHex(value, digits).split("");
|
||||
let v = parseInt(hexArr[i], 16);
|
||||
v = (v + delta) % 16;
|
||||
if (v < 0) v += 16;
|
||||
hexArr[i] = v.toString(16).toUpperCase();
|
||||
value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
|
||||
render();
|
||||
}
|
||||
|
||||
// dialog cancel / backdrop
|
||||
dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
|
||||
dialog.addEventListener("click", (e) => {
|
||||
const card = dialog.querySelector(".hex-dialog-card");
|
||||
if (card && !card.contains(e.target as Node)) closeDialog();
|
||||
});
|
||||
dialogInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") applyDialog();
|
||||
if (e.key === "Escape") closeDialog();
|
||||
});
|
||||
|
||||
// main click handler
|
||||
root.addEventListener("click", (e) => {
|
||||
const btn = (e.target as HTMLElement).closest<HTMLElement>("[data-action]");
|
||||
if (!btn) return;
|
||||
const action = btn.getAttribute("data-action")!;
|
||||
|
||||
if (action === "toggleToolbox") {
|
||||
toolbox.classList.toggle("is-open");
|
||||
toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false");
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
|
||||
if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
|
||||
|
||||
if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); }
|
||||
if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); }
|
||||
|
||||
if (action === "reset") { stopRandom(); value = 0; return render(); }
|
||||
if (action === "random") { return startRandom(); }
|
||||
|
||||
if (action === "customHex") return openDialog("hex");
|
||||
if (action === "customDenary") return openDialog("den");
|
||||
if (action === "customBinary") return openDialog("bin");
|
||||
|
||||
if (action === "dialogCancel") return closeDialog();
|
||||
if (action === "dialogApply") return applyDialog();
|
||||
|
||||
if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1);
|
||||
if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1);
|
||||
});
|
||||
|
||||
render();
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
const {
|
||||
title = "Computing:Box",
|
||||
} = Astro.props;
|
||||
const { title = "Computing:Box" } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -10,41 +8,10 @@ const {
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>{title}</title>
|
||||
<meta name="color-scheme" content="dark" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="siteHeader">
|
||||
<div class="siteHeaderInner">
|
||||
<a class="brand" href="/">
|
||||
<!-- Put your logo file here -->
|
||||
<img
|
||||
class="brandLogo"
|
||||
src="../images/computing-box-logo.svg"
|
||||
alt="Computing:Box logo"
|
||||
width="28"
|
||||
height="28"
|
||||
/>
|
||||
<span class="brandName">Computing:Box</span>
|
||||
</a>
|
||||
|
||||
<nav class="siteNav" aria-label="Main navigation">
|
||||
<a href="/about">About</a>
|
||||
<a href="/binary">Binary</a>
|
||||
<a href="/hexadecimal">Hexadecimal</a>
|
||||
<a href="/hex-colours">Hex Colours</a>
|
||||
<a href="/logic-gates">Logic Gates</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<slot />
|
||||
|
||||
<style>
|
||||
:root{
|
||||
/* 3× navbar height */
|
||||
--nav-height: 96px;
|
||||
--nav-pad-x: 22px;
|
||||
--nav-h: 108px; /* 3x-ish height */
|
||||
--bg: #1f2027;
|
||||
--text: #e8e8ee;
|
||||
--muted: #a9acb8;
|
||||
@@ -57,77 +24,95 @@ const {
|
||||
color:var(--text);
|
||||
}
|
||||
|
||||
.siteHeader{
|
||||
.siteNav{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
height: var(--nav-height);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
background: rgba(0,0,0,.15);
|
||||
z-index: 50;
|
||||
height: var(--nav-h);
|
||||
background: rgba(0,0,0,.10);
|
||||
border-bottom: 1px solid var(--line);
|
||||
backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.siteHeaderInner{
|
||||
width: 100%;
|
||||
.navInner{
|
||||
height: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--nav-pad-x);
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.brand{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap: 10px;
|
||||
gap:12px;
|
||||
text-decoration:none;
|
||||
color:var(--text);
|
||||
font-weight: 800;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.brandLogo{
|
||||
display:block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.brandName{
|
||||
font-size: 14px;
|
||||
opacity: .95;
|
||||
letter-spacing: .12em;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.siteNav{
|
||||
.navLinks{
|
||||
display:flex;
|
||||
gap: 18px;
|
||||
align-items:center;
|
||||
gap:18px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
.siteNav a{
|
||||
.navLinks a{
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
font-weight: 800;
|
||||
letter-spacing: .12em;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.siteNav a:hover{
|
||||
.navLinks a:hover{
|
||||
color: var(--text);
|
||||
background: rgba(255,255,255,.06);
|
||||
}
|
||||
|
||||
@media (max-width: 860px){
|
||||
.siteNav{ gap: 10px; }
|
||||
.siteNav a{ font-size: 10px; padding: 8px 6px; }
|
||||
.pageWrap{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="siteNav">
|
||||
<div class="navInner">
|
||||
<a class="brand" href="/">
|
||||
<img class="brandLogo" src="/images/computing-box-logo.svg" alt="Computing:Box logo" />
|
||||
<span class="brandName">COMPUTING:BOX</span>
|
||||
</a>
|
||||
|
||||
<nav class="navLinks" aria-label="Site navigation">
|
||||
<a href="/about">ABOUT</a>
|
||||
<a href="/binary">BINARY</a>
|
||||
<a href="/hexadecimal">HEXADECIMAL</a>
|
||||
<a href="/hex-colours">HEX COLOURS</a>
|
||||
<a href="/logic-gates">LOGIC GATES</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="pageWrap">
|
||||
<slot />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,20 +4,22 @@ import "../styles/binary.css";
|
||||
---
|
||||
|
||||
<BaseLayout title="Binary Simulator">
|
||||
<main class="wrap">
|
||||
<!-- Toolbox toggle sits below navbar (navbar is in BaseLayout) -->
|
||||
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
||||
<span class="toolboxLabel">TOOLBOX</span>
|
||||
<span class="toolboxText">TOOLBOX</span>
|
||||
</button>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="topGrid">
|
||||
<!-- LEFT -->
|
||||
<div>
|
||||
<div class="mainCol">
|
||||
<div class="readout">
|
||||
<div class="label">Denary</div>
|
||||
<div id="denaryNumber" class="num denaryValue">0</div>
|
||||
|
||||
<div class="label">Binary</div>
|
||||
<!-- NOTE: JS writes exact bit-width here, so initial value doesn't matter much -->
|
||||
<div id="binaryNumber" class="num binaryValue">00000000</div>
|
||||
</div>
|
||||
|
||||
@@ -28,19 +30,22 @@ import "../styles/binary.css";
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT TOOLBOX -->
|
||||
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
|
||||
<!-- SETTINGS -->
|
||||
<!-- RIGHT (Toolbox panel) -->
|
||||
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||
<!-- Settings -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Settings</div>
|
||||
|
||||
<div class="toggleRow">
|
||||
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
|
||||
|
||||
<label class="switch" aria-label="Toggle mode">
|
||||
<input id="modeToggle" type="checkbox" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<div class="toggleLabel" id="lblTwos">Two’s complement</div>
|
||||
|
||||
<!-- keep this on ONE line -->
|
||||
<div class="toggleLabel" id="lblTwos">Two's complement</div>
|
||||
</div>
|
||||
|
||||
<div class="hint" id="modeHint">
|
||||
@@ -49,6 +54,7 @@ import "../styles/binary.css";
|
||||
|
||||
<div class="subCard">
|
||||
<div class="subTitle">Bit width</div>
|
||||
|
||||
<div class="bitWidthRow">
|
||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
||||
|
||||
@@ -72,37 +78,34 @@ import "../styles/binary.css";
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CUSTOM -->
|
||||
<!-- Custom Number -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Custom</div>
|
||||
<div class="cardTitle">Custom Number</div>
|
||||
|
||||
<div class="twoBtnRow">
|
||||
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
<div class="controlsRow">
|
||||
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||
</div>
|
||||
|
||||
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
|
||||
Random
|
||||
</button>
|
||||
|
||||
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||
</div>
|
||||
|
||||
<!-- TOOLS -->
|
||||
<!-- Tools -->
|
||||
<div class="card">
|
||||
<div class="cardTitle">Tools</div>
|
||||
|
||||
<div class="toolsTopRow">
|
||||
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||
<div class="toolRowCentered">
|
||||
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||
</div>
|
||||
|
||||
<div class="twoBtnRow">
|
||||
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
||||
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
||||
<div class="toolRow2">
|
||||
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
|
||||
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
|
||||
</div>
|
||||
|
||||
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
|
||||
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
8
src/pages/hexadecimal.astro
Normal file
8
src/pages/hexadecimal.astro
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import HexSimulator from "../components/simulators/HexSimulator.astro";
|
||||
---
|
||||
|
||||
<BaseLayout title="Hexadecimal | Computing:Box">
|
||||
<HexSimulator />
|
||||
</BaseLayout>
|
||||
@@ -34,9 +34,12 @@
|
||||
----------------------------- */
|
||||
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||
let bits = new Array(bitCount).fill(false);
|
||||
|
||||
let randomTimer = null;
|
||||
|
||||
// For responsive wrapping of the top binary display
|
||||
let nibblesPerLine = null;
|
||||
let wrapMeasureSpan = null;
|
||||
|
||||
/* -----------------------------
|
||||
HELPERS
|
||||
----------------------------- */
|
||||
@@ -80,6 +83,7 @@
|
||||
function unsignedBigIntToBits(vUnsigned) {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = ((vUnsigned % span) + span) % span;
|
||||
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
|
||||
}
|
||||
@@ -98,16 +102,6 @@
|
||||
unsignedBigIntToBits(v);
|
||||
}
|
||||
|
||||
function formatBinaryGrouped() {
|
||||
let s = "";
|
||||
for (let i = bitCount - 1; i >= 0; i--) {
|
||||
s += bits[i] ? "1" : "0";
|
||||
const posFromRight = (bitCount - i);
|
||||
if (i !== 0 && posFromRight % 4 === 0) s += " ";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function updateModeHint() {
|
||||
if (!modeHint) return;
|
||||
modeHint.textContent = isTwosMode()
|
||||
@@ -115,6 +109,72 @@
|
||||
: "Tip: In unsigned binary, all bits represent positive values.";
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
TOP BINARY DISPLAY: responsive wrap by nibble count
|
||||
----------------------------- */
|
||||
function ensureWrapMeasurer() {
|
||||
if (wrapMeasureSpan || !binaryEl) return;
|
||||
wrapMeasureSpan = document.createElement("span");
|
||||
wrapMeasureSpan.style.position = "absolute";
|
||||
wrapMeasureSpan.style.visibility = "hidden";
|
||||
wrapMeasureSpan.style.whiteSpace = "pre";
|
||||
wrapMeasureSpan.style.pointerEvents = "none";
|
||||
// Inherit font/letterspacing from binaryEl
|
||||
wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font;
|
||||
wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing;
|
||||
document.body.appendChild(wrapMeasureSpan);
|
||||
}
|
||||
|
||||
function computeNibblesPerLine() {
|
||||
if (!binaryEl) return null;
|
||||
ensureWrapMeasurer();
|
||||
|
||||
// Available width = width of the readout area (binaryEl parent)
|
||||
const host = binaryEl.parentElement;
|
||||
if (!host) return null;
|
||||
|
||||
const hostW = host.getBoundingClientRect().width;
|
||||
if (!Number.isFinite(hostW) || hostW <= 0) return null;
|
||||
|
||||
// Measure one nibble including trailing space ("0000 ")
|
||||
wrapMeasureSpan.textContent = "0000 ";
|
||||
const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1;
|
||||
|
||||
// Safety: keep at least 1 nibble per line
|
||||
const max = Math.max(1, Math.floor(hostW / nibbleW));
|
||||
return max;
|
||||
}
|
||||
|
||||
function formatBinaryWrapped() {
|
||||
// EXACT bitCount digits (no padding to 4)
|
||||
let raw = "";
|
||||
for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0";
|
||||
|
||||
// If <= 4 bits, do NOT insert spaces/newlines at all
|
||||
if (bitCount <= 4) return raw;
|
||||
|
||||
const groups = [];
|
||||
for (let i = 0; i < raw.length; i += 4) {
|
||||
groups.push(raw.slice(i, i + 4));
|
||||
}
|
||||
|
||||
const perLine = nibblesPerLine ?? groups.length;
|
||||
if (perLine >= groups.length) return groups.join(" ");
|
||||
|
||||
const lines = [];
|
||||
for (let i = 0; i < groups.length; i += perLine) {
|
||||
lines.push(groups.slice(i, i + perLine).join(" "));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function refreshBinaryWrap() {
|
||||
const next = computeNibblesPerLine();
|
||||
// Only update if it actually changes (prevents jitter)
|
||||
if (next !== nibblesPerLine) nibblesPerLine = next;
|
||||
updateReadout(); // re-render with new wrap
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
BUILD UI (BITS)
|
||||
----------------------------- */
|
||||
@@ -128,9 +188,17 @@
|
||||
|
||||
bitsGrid.innerHTML = "";
|
||||
|
||||
bitsGrid.classList.toggle("bitsFew", bitCount < 8);
|
||||
if (bitCount < 8) {
|
||||
bitsGrid.style.setProperty("--cols", String(bitCount));
|
||||
} else {
|
||||
bitsGrid.style.removeProperty("--cols");
|
||||
}
|
||||
|
||||
for (let i = bitCount - 1; i >= 0; i--) {
|
||||
const bitEl = document.createElement("div");
|
||||
bitEl.className = "bit";
|
||||
|
||||
bitEl.innerHTML = `
|
||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
||||
<div class="bitVal" id="bitLabel-${i}"></div>
|
||||
@@ -139,6 +207,7 @@
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
`;
|
||||
|
||||
bitsGrid.appendChild(bitEl);
|
||||
}
|
||||
|
||||
@@ -150,6 +219,28 @@
|
||||
});
|
||||
});
|
||||
|
||||
// bulb styling + 25% bigger (vs 26px previously)
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const bulb = document.getElementById(`bulb-${i}`);
|
||||
if (!bulb) continue;
|
||||
bulb.style.width = "auto";
|
||||
bulb.style.height = "auto";
|
||||
bulb.style.border = "none";
|
||||
bulb.style.background = "transparent";
|
||||
bulb.style.borderRadius = "0";
|
||||
bulb.style.boxShadow = "none";
|
||||
bulb.style.opacity = "0.45";
|
||||
bulb.style.fontSize = "32px";
|
||||
bulb.style.lineHeight = "1";
|
||||
bulb.style.display = "flex";
|
||||
bulb.style.alignItems = "center";
|
||||
bulb.style.justifyContent = "center";
|
||||
bulb.style.filter = "grayscale(1)";
|
||||
bulb.textContent = "💡";
|
||||
}
|
||||
|
||||
// wrapping may change when bit width changes
|
||||
refreshBinaryWrap();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@@ -161,10 +252,8 @@
|
||||
const label = document.getElementById(`bitLabel-${i}`);
|
||||
if (!label) continue;
|
||||
|
||||
// Keep label on ONE LINE (no wrapping)
|
||||
label.style.whiteSpace = "nowrap";
|
||||
|
||||
if (isTwosMode() && i === bitCount - 1) {
|
||||
// Keep on one line (CSS: white-space:nowrap)
|
||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
||||
} else {
|
||||
label.textContent = pow2Big(i).toString();
|
||||
@@ -183,14 +272,25 @@
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const bulb = document.getElementById(`bulb-${i}`);
|
||||
if (!bulb) continue;
|
||||
bulb.classList.toggle("on", bits[i] === true);
|
||||
|
||||
const on = bits[i] === true;
|
||||
if (on) {
|
||||
bulb.style.opacity = "1";
|
||||
bulb.style.filter = "grayscale(0)";
|
||||
bulb.style.textShadow = "0 0 18px rgba(255,216,107,.75), 0 0 30px rgba(255,216,107,.45)";
|
||||
} else {
|
||||
bulb.style.opacity = "0.45";
|
||||
bulb.style.filter = "grayscale(1)";
|
||||
bulb.style.textShadow = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateReadout() {
|
||||
if (!denaryEl || !binaryEl) return;
|
||||
|
||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
||||
binaryEl.textContent = formatBinaryGrouped();
|
||||
binaryEl.textContent = formatBinaryWrapped();
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
@@ -202,16 +302,18 @@
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
INPUT SETTERS
|
||||
SET FROM INPUT
|
||||
----------------------------- */
|
||||
function setFromBinaryString(binStr) {
|
||||
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
||||
if (!/^[01]+$/.test(clean)) return false;
|
||||
|
||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const charFromRight = padded[padded.length - 1 - i];
|
||||
bits[i] = charFromRight === "1";
|
||||
}
|
||||
|
||||
updateUI();
|
||||
return true;
|
||||
}
|
||||
@@ -253,13 +355,15 @@
|
||||
}
|
||||
|
||||
function shiftRight() {
|
||||
// Unsigned: logical right shift (MSB becomes 0)
|
||||
// Two's complement: arithmetic right shift (MSB preserved)
|
||||
if (isTwosMode()) {
|
||||
// arithmetic right shift: keep MSB
|
||||
const msb = bits[bitCount - 1];
|
||||
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
|
||||
bits[bitCount - 1] = isTwosMode() ? msb : false;
|
||||
bits[bitCount - 1] = msb;
|
||||
} else {
|
||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||
bits[bitCount - 1] = false;
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@@ -280,8 +384,7 @@
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = (bitsToUnsignedBigInt() + 1n) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
@@ -295,18 +398,16 @@
|
||||
signedBigIntToBitsTwos(v);
|
||||
} else {
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
|
||||
unsignedBigIntToBits(v);
|
||||
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
RANDOM (with running pulse + longer run)
|
||||
RANDOM
|
||||
----------------------------- */
|
||||
function cryptoRandomBigInt(maxExclusive) {
|
||||
if (maxExclusive <= 0n) return 0n;
|
||||
|
||||
const bitLen = maxExclusive.toString(2).length;
|
||||
const byteLen = Math.ceil(bitLen / 8);
|
||||
|
||||
@@ -319,13 +420,12 @@
|
||||
|
||||
const extraBits = BigInt(byteLen * 8 - bitLen);
|
||||
if (extraBits > 0n) x = x >> extraBits;
|
||||
|
||||
if (x < maxExclusive) return x;
|
||||
}
|
||||
}
|
||||
|
||||
function setRandomOnce() {
|
||||
const span = unsignedMaxExclusive(bitCount); // 2^n
|
||||
const span = unsignedMaxExclusive(bitCount);
|
||||
const u = cryptoRandomBigInt(span);
|
||||
unsignedBigIntToBits(u);
|
||||
updateUI();
|
||||
@@ -337,11 +437,8 @@
|
||||
randomTimer = null;
|
||||
}
|
||||
|
||||
// pulse while running
|
||||
btnRandom?.classList.add("is-running");
|
||||
|
||||
const start = Date.now();
|
||||
const durationMs = 1125; // 25% longer than 900ms
|
||||
const durationMs = 1125; // (your “~25% longer” vs 900ms)
|
||||
const tickMs = 80;
|
||||
|
||||
randomTimer = setInterval(() => {
|
||||
@@ -349,31 +446,30 @@
|
||||
if (Date.now() - start >= durationMs) {
|
||||
clearInterval(randomTimer);
|
||||
randomTimer = null;
|
||||
btnRandom?.classList.remove("is-running");
|
||||
}
|
||||
}, tickMs);
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
BIT WIDTH
|
||||
BIT WIDTH CONTROLS
|
||||
----------------------------- */
|
||||
function setBitWidth(n) {
|
||||
buildBits(clampInt(n, 1, 64));
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
TOOLBOX VISIBILITY
|
||||
TOOLBOX TOGGLE (simple open/close state)
|
||||
----------------------------- */
|
||||
function setToolboxVisible(isVisible) {
|
||||
if (!toolboxPanel) return;
|
||||
toolboxPanel.style.display = isVisible ? "flex" : "none";
|
||||
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
|
||||
function setToolboxOpen(open) {
|
||||
document.body.classList.toggle("toolboxClosed", !open);
|
||||
toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
refreshBinaryWrap(); // width changes when toolbox closes/opens
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
EVENTS
|
||||
----------------------------- */
|
||||
modeToggle?.addEventListener("change", updateUI);
|
||||
modeToggle?.addEventListener("change", () => updateUI());
|
||||
|
||||
btnCustomBinary?.addEventListener("click", () => {
|
||||
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
|
||||
@@ -384,8 +480,8 @@
|
||||
btnCustomDenary?.addEventListener("click", () => {
|
||||
const v = prompt(
|
||||
isTwosMode()
|
||||
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
|
||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
|
||||
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
|
||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
|
||||
);
|
||||
if (v === null) return;
|
||||
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
||||
@@ -406,8 +502,15 @@
|
||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
||||
|
||||
toolboxToggle?.addEventListener("click", () => {
|
||||
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
|
||||
setToolboxVisible(!isOpen);
|
||||
const isOpen = !document.body.classList.contains("toolboxClosed");
|
||||
setToolboxOpen(!isOpen);
|
||||
});
|
||||
|
||||
// Recompute wrapping live when the window size changes
|
||||
let resizeT = null;
|
||||
window.addEventListener("resize", () => {
|
||||
if (resizeT) clearTimeout(resizeT);
|
||||
resizeT = setTimeout(() => refreshBinaryWrap(), 60);
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
@@ -415,5 +518,5 @@
|
||||
----------------------------- */
|
||||
updateModeHint();
|
||||
buildBits(bitCount);
|
||||
setToolboxVisible(true);
|
||||
setToolboxOpen(true);
|
||||
})();
|
||||
|
||||
@@ -1,314 +1,227 @@
|
||||
/* Binary page styles (moved OUT of binary.astro) */
|
||||
|
||||
:root{
|
||||
--bg: #1f2027;
|
||||
--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);
|
||||
}
|
||||
|
||||
/* NUMBERS 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;
|
||||
}
|
||||
|
||||
/* TEXT font */
|
||||
@font-face{
|
||||
font-family: "SevenSegment";
|
||||
src:
|
||||
url("/fonts/Seven-Segment.woff2") format("woff2"),
|
||||
url("/fonts/Seven-Segment.woff") format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
--panel-w: 360px;
|
||||
--gap: 22px;
|
||||
}
|
||||
|
||||
.wrap{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 26px 22px 60px;
|
||||
padding: 26px 20px 48px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* MAIN GRID */
|
||||
.topGrid{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 28px;
|
||||
grid-template-columns: 1fr var(--panel-w);
|
||||
gap: var(--gap);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Toolbox toggle BELOW navbar */
|
||||
.toolboxToggle{
|
||||
position: sticky;
|
||||
top: calc(var(--nav-height) + 10px);
|
||||
z-index: 900;
|
||||
float: right;
|
||||
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap: 10px;
|
||||
|
||||
background: rgba(255,255,255,.06);
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
color: #fff;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
letter-spacing: .10em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
/* When toolbox is hidden, reclaim space + centre content */
|
||||
body.toolboxClosed .topGrid{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
body.toolboxClosed #toolboxPanel{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mainCol{
|
||||
min-width: 0;
|
||||
}
|
||||
.toolboxToggle:hover{ background: rgba(255,255,255,.09); }
|
||||
|
||||
/* Readout */
|
||||
.readout{
|
||||
text-align: center;
|
||||
padding: 10px 10px 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.label{
|
||||
letter-spacing: .18em;
|
||||
font-weight: 900;
|
||||
color: var(--muted);
|
||||
opacity: .8;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
|
||||
.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);
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
white-space: pre-line; /* allows JS \n wraps */
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.denaryValue{
|
||||
font-size: 52px;
|
||||
line-height: 1.0;
|
||||
font-size: 54px;
|
||||
margin: 6px 0 10px;
|
||||
}
|
||||
|
||||
.binaryValue{
|
||||
font-size: 46px;
|
||||
letter-spacing: .14em;
|
||||
line-height: 1.05;
|
||||
margin: 6px 0 14px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 56px;
|
||||
margin: 4px 0 18px;
|
||||
}
|
||||
|
||||
.divider{
|
||||
margin-top: 26px;
|
||||
border-top: 1px solid var(--line);
|
||||
height: 1px;
|
||||
background: rgba(255,255,255,.10);
|
||||
margin: 14px auto 24px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
/* RIGHT COLUMN */
|
||||
.bitsWrap{
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.bitsGrid{
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Default: a single row of bits (will wrap automatically as bit count grows) */
|
||||
.bitsGrid{
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.bitsGrid.bitsFew{
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Bit tile */
|
||||
.bit{
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bulb{
|
||||
font-size: 32px; /* JS also bumps this */
|
||||
line-height: 1;
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
.bitVal{
|
||||
font-size: 22px;
|
||||
line-height: 1.05;
|
||||
text-align: center;
|
||||
white-space: nowrap; /* keep -128 on one line */
|
||||
}
|
||||
|
||||
/* Switch (existing classes assumed) */
|
||||
.switch{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
}
|
||||
.switch input{ display:none; }
|
||||
.slider{
|
||||
position:absolute;
|
||||
inset:0;
|
||||
border-radius:999px;
|
||||
background: rgba(255,255,255,.18);
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
}
|
||||
.slider:before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
left: 3px;
|
||||
top: 2.5px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
transition: transform .18s ease;
|
||||
}
|
||||
.switch input:checked + .slider:before{
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* Toolbox toggle button */
|
||||
.toolboxToggle{
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 18px;
|
||||
z-index: 20;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
background: rgba(255,255,255,.06);
|
||||
color: rgba(255,255,255,.92);
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolboxText{
|
||||
letter-spacing: .12em;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* Toolbox panel */
|
||||
.panelCol{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap: 14px;
|
||||
position: sticky;
|
||||
top: calc(var(--nav-h, 72px) + 18px);
|
||||
align-self: start;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card{
|
||||
background: var(--panel2);
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,.05);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.cardTitle{
|
||||
letter-spacing: .18em;
|
||||
font-weight: 900;
|
||||
color: var(--muted);
|
||||
opacity: .8;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
margin: 0 0 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hint{
|
||||
opacity: .7;
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Keep mode labels on one line */
|
||||
.toggleRow{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.toggleLabel{
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subCard{
|
||||
margin-top: 12px;
|
||||
background: rgba(255,255,255,.04);
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(0,0,0,.12);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.subTitle{
|
||||
letter-spacing: .18em;
|
||||
font-weight: 900;
|
||||
color: var(--muted);
|
||||
opacity: .8;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
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: 900;
|
||||
font-size: 13px;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Switch */
|
||||
.switch{
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 34px;
|
||||
display:inline-block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.switch input{ 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);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.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: 900;
|
||||
cursor: pointer;
|
||||
letter-spacing: .10em;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn:active{ transform: translateY(1px); }
|
||||
|
||||
.btnAccent{
|
||||
background: rgba(51,255,122,.18);
|
||||
border-color: rgba(51,255,122,.45);
|
||||
}
|
||||
|
||||
.customRow{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btnRandom{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Random pulse ONLY while running */
|
||||
@keyframes pulseGreen {
|
||||
0% { box-shadow: 0 0 0 rgba(51,255,122,0); }
|
||||
50% { box-shadow: 0 0 18px rgba(51,255,122,.55), 0 0 28px rgba(51,255,122,.25); }
|
||||
100% { box-shadow: 0 0 0 rgba(51,255,122,0); }
|
||||
}
|
||||
.btnRandom.is-running{
|
||||
animation: pulseGreen 0.9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Tools */
|
||||
.toolRowCenter{
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toolBtn{
|
||||
height: 48px;
|
||||
width: 66px;
|
||||
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: 22px;
|
||||
}
|
||||
|
||||
.toolDec{
|
||||
background: rgba(255,0,0,.18);
|
||||
border-color: rgba(255,0,0,.35);
|
||||
}
|
||||
.toolInc{
|
||||
background: rgba(51,255,122,.18);
|
||||
border-color: rgba(51,255,122,.35);
|
||||
}
|
||||
|
||||
.shiftRow{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Reset full-width + red hover pulse/background */
|
||||
.btnReset{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes pulseRedBg {
|
||||
0% { box-shadow: 0 0 0 rgba(255,0,0,0); background: rgba(255,0,0,.14); }
|
||||
50% { box-shadow: 0 0 18px rgba(255,0,0,.45), 0 0 28px rgba(255,0,0,.20); background: rgba(255,0,0,.28); }
|
||||
100% { box-shadow: 0 0 0 rgba(255,0,0,0); background: rgba(255,0,0,.14); }
|
||||
}
|
||||
.btnReset:hover{
|
||||
border-color: rgba(255,0,0,.45);
|
||||
animation: pulseRedBg 0.9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Bit width control */
|
||||
.bitWidthRow{
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 44px;
|
||||
@@ -316,97 +229,98 @@ body{
|
||||
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;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,.04);
|
||||
}
|
||||
.bitInputLabel{
|
||||
color:var(--muted);
|
||||
font-size:12px;
|
||||
font-weight:900;
|
||||
letter-spacing:.18em;
|
||||
opacity: .75;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bitInput{
|
||||
width: 86px;
|
||||
text-align:right;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
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;
|
||||
color: inherit;
|
||||
font-size: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Bits grid */
|
||||
.bitsWrap{ margin-top: 22px; }
|
||||
.miniBtn{
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
color: rgba(255,255,255,.9);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bitsGrid{
|
||||
/* Buttons */
|
||||
.controlsRow{
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
justify-content:center;
|
||||
grid-template-columns: repeat(8, minmax(92px, 1fr));
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.bit{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
padding: 8px 4px;
|
||||
text-align:center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Bulb 25% bigger */
|
||||
.bulb{
|
||||
font-size: 32px; /* was ~26 */
|
||||
line-height: 1;
|
||||
opacity: .45;
|
||||
filter: grayscale(1);
|
||||
.btn{
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
color: rgba(255,255,255,.92);
|
||||
padding: 12px 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .10em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bulb.on{
|
||||
opacity: 1;
|
||||
filter: grayscale(0);
|
||||
text-shadow: 0 0 14px rgba(255,216,107,.75), 0 0 26px rgba(255,216,107,.45);
|
||||
.btnWide{ width: 100%; }
|
||||
|
||||
.btnAccent{
|
||||
background: rgba(0,255,140,.12);
|
||||
border-color: rgba(0,255,140,.22);
|
||||
}
|
||||
|
||||
.bitVal{
|
||||
font-family:"DSEG7ClassicRegular", ui-monospace, monospace;
|
||||
font-size: 28px;
|
||||
color: var(--text);
|
||||
opacity: .95;
|
||||
line-height: 1.05;
|
||||
min-height: 32px;
|
||||
|
||||
/* prevent “run together” when huge values */
|
||||
white-space: nowrap;
|
||||
.toolRowCentered{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 10px 0 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px){
|
||||
.topGrid{ grid-template-columns: 1fr; }
|
||||
.toolboxToggle{ float:none; }
|
||||
.toolBtn{
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
color: rgba(255,255,255,.92);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); }
|
||||
.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); }
|
||||
|
||||
.toolRow2{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Reset stays white text */
|
||||
.btnReset{
|
||||
color: rgba(255,255,255,.92);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user