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 {
|
const { title = "Computing:Box" } = Astro.props;
|
||||||
title = "Computing:Box",
|
|
||||||
} = Astro.props;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -10,41 +8,10 @@ const {
|
|||||||
<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>
|
||||||
<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>
|
<style>
|
||||||
:root{
|
:root{
|
||||||
/* 3× navbar height */
|
--nav-h: 108px; /* 3x-ish height */
|
||||||
--nav-height: 96px;
|
|
||||||
--nav-pad-x: 22px;
|
|
||||||
--bg: #1f2027;
|
--bg: #1f2027;
|
||||||
--text: #e8e8ee;
|
--text: #e8e8ee;
|
||||||
--muted: #a9acb8;
|
--muted: #a9acb8;
|
||||||
@@ -53,81 +20,99 @@ const {
|
|||||||
|
|
||||||
body{
|
body{
|
||||||
margin:0;
|
margin:0;
|
||||||
background: var(--bg);
|
background:var(--bg);
|
||||||
color: var(--text);
|
color:var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeader{
|
.siteNav{
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 50;
|
||||||
height: var(--nav-height);
|
height: var(--nav-h);
|
||||||
display:flex;
|
background: rgba(0,0,0,.10);
|
||||||
align-items:center;
|
|
||||||
background: rgba(0,0,0,.15);
|
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeaderInner{
|
.navInner{
|
||||||
width: 100%;
|
height: 100%;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 var(--nav-pad-x);
|
padding: 0 20px;
|
||||||
display:flex;
|
display: flex;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
justify-content:space-between;
|
justify-content: space-between;
|
||||||
gap: 18px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand{
|
.brand{
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
gap: 10px;
|
gap:12px;
|
||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
color: var(--text);
|
color:var(--text);
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: .08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandLogo{
|
.brandLogo{
|
||||||
display:block;
|
width: 2em;
|
||||||
width: 28px;
|
height: 2em;
|
||||||
height: 28px;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandName{
|
.brandName{
|
||||||
font-size: 14px;
|
letter-spacing: .12em;
|
||||||
opacity: .95;
|
font-weight: 900;
|
||||||
}
|
|
||||||
|
|
||||||
.siteNav{
|
|
||||||
display:flex;
|
|
||||||
gap: 18px;
|
|
||||||
align-items:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteNav a{
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration:none;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: .08em;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 11px;
|
font-size: 18px;
|
||||||
padding: 10px 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteNav a:hover{
|
.navLinks{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:18px;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLinks a{
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .12em;
|
||||||
|
font-size: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLinks a:hover{
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: rgba(255,255,255,.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px){
|
.pageWrap{
|
||||||
.siteNav{ gap: 10px; }
|
max-width: 1400px;
|
||||||
.siteNav a{ font-size: 10px; padding: 8px 6px; }
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,21 +4,23 @@ import "../styles/binary.css";
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Binary Simulator">
|
<BaseLayout title="Binary Simulator">
|
||||||
<button id="toolboxToggle" class="toolboxToggle" type="button" aria-expanded="true">
|
|
||||||
<span class="toolboxIcon" aria-hidden="true">🧰</span>
|
|
||||||
<span class="toolboxLabel">TOOLBOX</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<main class="wrap">
|
<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="toolboxText">TOOLBOX</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<section class="topGrid">
|
<section class="topGrid">
|
||||||
<!-- LEFT -->
|
<!-- LEFT -->
|
||||||
<div>
|
<div class="mainCol">
|
||||||
<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 denaryValue">0</div>
|
||||||
|
|
||||||
<div class="label">Binary</div>
|
<div class="label">Binary</div>
|
||||||
<div id="binaryNumber" class="num binaryValue">0000 0000</div>
|
<!-- NOTE: JS writes exact bit-width here, so initial value doesn't matter much -->
|
||||||
|
<div id="binaryNumber" class="num binaryValue">00000000</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@@ -28,19 +30,22 @@ import "../styles/binary.css";
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT TOOLBOX -->
|
<!-- RIGHT (Toolbox panel) -->
|
||||||
<aside id="toolbox" class="panelCol" aria-label="Toolbox">
|
<aside id="toolboxPanel" class="panelCol" aria-label="Toolbox">
|
||||||
<!-- SETTINGS -->
|
<!-- Settings -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Settings</div>
|
<div class="cardTitle">Settings</div>
|
||||||
|
|
||||||
<div class="toggleRow">
|
<div class="toggleRow">
|
||||||
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
|
<div class="toggleLabel" id="lblUnsigned">Unsigned</div>
|
||||||
|
|
||||||
<label class="switch" aria-label="Toggle mode">
|
<label class="switch" aria-label="Toggle mode">
|
||||||
<input id="modeToggle" type="checkbox" />
|
<input id="modeToggle" type="checkbox" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<div class="hint" id="modeHint">
|
<div class="hint" id="modeHint">
|
||||||
@@ -49,6 +54,7 @@ import "../styles/binary.css";
|
|||||||
|
|
||||||
<div class="subCard">
|
<div class="subCard">
|
||||||
<div class="subTitle">Bit width</div>
|
<div class="subTitle">Bit width</div>
|
||||||
|
|
||||||
<div class="bitWidthRow">
|
<div class="bitWidthRow">
|
||||||
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
<button class="miniBtn" id="btnBitsDown" type="button" aria-label="Decrease bits">−</button>
|
||||||
|
|
||||||
@@ -72,37 +78,34 @@ import "../styles/binary.css";
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CUSTOM -->
|
<!-- Custom Number -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Custom</div>
|
<div class="cardTitle">Custom Number</div>
|
||||||
|
|
||||||
<div class="twoBtnRow">
|
<div class="controlsRow">
|
||||||
<button class="btn btnAccent" id="btnCustomBinary" type="button">Custom Binary</button>
|
<button class="btn btnAccent btnHalf" id="btnCustomBinary" type="button">Custom Binary</button>
|
||||||
<button class="btn btnAccent" id="btnCustomDenary" type="button">Custom Denary</button>
|
<button class="btn btnAccent btnHalf" id="btnCustomDenary" type="button">Custom Denary</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="toolBtn toolWide toolRandom" id="btnRandom" type="button">
|
<button class="btn btnWide" id="btnRandom" type="button">Random</button>
|
||||||
Random
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="hint">Random runs briefly then stops automatically.</div>
|
<div class="hint">Random runs briefly then stops automatically.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TOOLS -->
|
<!-- Tools -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="cardTitle">Tools</div>
|
<div class="cardTitle">Tools</div>
|
||||||
|
|
||||||
<div class="toolsTopRow">
|
<div class="toolRowCentered">
|
||||||
<button class="toolBtn toolArrow toolDown" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
<button class="toolBtn toolSpin toolDec" id="btnDec" type="button" aria-label="Decrement">▼</button>
|
||||||
<button class="toolBtn toolArrow toolUp" id="btnInc" type="button" aria-label="Increment">▲</button>
|
<button class="toolBtn toolSpin toolInc" id="btnInc" type="button" aria-label="Increment">▲</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="twoBtnRow">
|
<div class="toolRow2">
|
||||||
<button class="btn" id="btnShiftLeft" type="button">Left Shift</button>
|
<button class="btn btnHalf" id="btnShiftLeft" type="button">Left Shift</button>
|
||||||
<button class="btn" id="btnShiftRight" type="button">Right Shift</button>
|
<button class="btn btnHalf" id="btnShiftRight" type="button">Right Shift</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="toolBtn toolWide toolReset" id="btnClear" type="button">Reset</button>
|
<button class="btn btnReset btnWide" id="btnClear" type="button">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</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 bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
|
||||||
let bits = new Array(bitCount).fill(false);
|
let bits = new Array(bitCount).fill(false);
|
||||||
|
|
||||||
let randomTimer = null;
|
let randomTimer = null;
|
||||||
|
|
||||||
|
// For responsive wrapping of the top binary display
|
||||||
|
let nibblesPerLine = null;
|
||||||
|
let wrapMeasureSpan = null;
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
HELPERS
|
HELPERS
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -80,6 +83,7 @@
|
|||||||
function unsignedBigIntToBits(vUnsigned) {
|
function unsignedBigIntToBits(vUnsigned) {
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const v = ((vUnsigned % span) + span) % span;
|
const v = ((vUnsigned % span) + span) % span;
|
||||||
|
|
||||||
for (let i = 0; i < bitCount; i++) {
|
for (let i = 0; i < bitCount; i++) {
|
||||||
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
|
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
|
||||||
}
|
}
|
||||||
@@ -98,16 +102,6 @@
|
|||||||
unsignedBigIntToBits(v);
|
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() {
|
function updateModeHint() {
|
||||||
if (!modeHint) return;
|
if (!modeHint) return;
|
||||||
modeHint.textContent = isTwosMode()
|
modeHint.textContent = isTwosMode()
|
||||||
@@ -115,6 +109,72 @@
|
|||||||
: "Tip: In unsigned binary, all bits represent positive values.";
|
: "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)
|
BUILD UI (BITS)
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -128,9 +188,17 @@
|
|||||||
|
|
||||||
bitsGrid.innerHTML = "";
|
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--) {
|
for (let i = bitCount - 1; i >= 0; i--) {
|
||||||
const bitEl = document.createElement("div");
|
const bitEl = document.createElement("div");
|
||||||
bitEl.className = "bit";
|
bitEl.className = "bit";
|
||||||
|
|
||||||
bitEl.innerHTML = `
|
bitEl.innerHTML = `
|
||||||
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
<div class="bulb" id="bulb-${i}" aria-hidden="true">💡</div>
|
||||||
<div class="bitVal" id="bitLabel-${i}"></div>
|
<div class="bitVal" id="bitLabel-${i}"></div>
|
||||||
@@ -139,6 +207,7 @@
|
|||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
bitsGrid.appendChild(bitEl);
|
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();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,10 +252,8 @@
|
|||||||
const label = document.getElementById(`bitLabel-${i}`);
|
const label = document.getElementById(`bitLabel-${i}`);
|
||||||
if (!label) continue;
|
if (!label) continue;
|
||||||
|
|
||||||
// Keep label on ONE LINE (no wrapping)
|
|
||||||
label.style.whiteSpace = "nowrap";
|
|
||||||
|
|
||||||
if (isTwosMode() && i === bitCount - 1) {
|
if (isTwosMode() && i === bitCount - 1) {
|
||||||
|
// Keep on one line (CSS: white-space:nowrap)
|
||||||
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
|
||||||
} else {
|
} else {
|
||||||
label.textContent = pow2Big(i).toString();
|
label.textContent = pow2Big(i).toString();
|
||||||
@@ -183,14 +272,25 @@
|
|||||||
for (let i = 0; i < bitCount; i++) {
|
for (let i = 0; i < bitCount; i++) {
|
||||||
const bulb = document.getElementById(`bulb-${i}`);
|
const bulb = document.getElementById(`bulb-${i}`);
|
||||||
if (!bulb) continue;
|
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() {
|
function updateReadout() {
|
||||||
if (!denaryEl || !binaryEl) return;
|
if (!denaryEl || !binaryEl) return;
|
||||||
|
|
||||||
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
|
||||||
binaryEl.textContent = formatBinaryGrouped();
|
binaryEl.textContent = formatBinaryWrapped();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
@@ -202,16 +302,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
INPUT SETTERS
|
SET FROM INPUT
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function setFromBinaryString(binStr) {
|
function setFromBinaryString(binStr) {
|
||||||
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
const clean = String(binStr ?? "").replace(/\s+/g, "");
|
||||||
if (!/^[01]+$/.test(clean)) return false;
|
if (!/^[01]+$/.test(clean)) return false;
|
||||||
|
|
||||||
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
|
||||||
for (let i = 0; i < bitCount; i++) {
|
for (let i = 0; i < bitCount; i++) {
|
||||||
const charFromRight = padded[padded.length - 1 - i];
|
const charFromRight = padded[padded.length - 1 - i];
|
||||||
bits[i] = charFromRight === "1";
|
bits[i] = charFromRight === "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI();
|
updateUI();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -253,13 +355,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shiftRight() {
|
function shiftRight() {
|
||||||
// Unsigned: logical right shift (MSB becomes 0)
|
if (isTwosMode()) {
|
||||||
// Two's complement: arithmetic right shift (MSB preserved)
|
// arithmetic right shift: keep MSB
|
||||||
const msb = bits[bitCount - 1];
|
const msb = bits[bitCount - 1];
|
||||||
|
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||||
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
bits[bitCount - 1] = msb;
|
||||||
|
} else {
|
||||||
bits[bitCount - 1] = isTwosMode() ? msb : false;
|
for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
|
||||||
|
bits[bitCount - 1] = false;
|
||||||
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,8 +384,7 @@
|
|||||||
signedBigIntToBitsTwos(v);
|
signedBigIntToBitsTwos(v);
|
||||||
} else {
|
} else {
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const v = (bitsToUnsignedBigInt() + 1n) % span;
|
unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -295,18 +398,16 @@
|
|||||||
signedBigIntToBitsTwos(v);
|
signedBigIntToBitsTwos(v);
|
||||||
} else {
|
} else {
|
||||||
const span = unsignedMaxExclusive(bitCount);
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const v = (bitsToUnsignedBigInt() - 1n + span) % span;
|
unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
|
||||||
unsignedBigIntToBits(v);
|
|
||||||
}
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
RANDOM (with running pulse + longer run)
|
RANDOM
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function cryptoRandomBigInt(maxExclusive) {
|
function cryptoRandomBigInt(maxExclusive) {
|
||||||
if (maxExclusive <= 0n) return 0n;
|
if (maxExclusive <= 0n) return 0n;
|
||||||
|
|
||||||
const bitLen = maxExclusive.toString(2).length;
|
const bitLen = maxExclusive.toString(2).length;
|
||||||
const byteLen = Math.ceil(bitLen / 8);
|
const byteLen = Math.ceil(bitLen / 8);
|
||||||
|
|
||||||
@@ -319,13 +420,12 @@
|
|||||||
|
|
||||||
const extraBits = BigInt(byteLen * 8 - bitLen);
|
const extraBits = BigInt(byteLen * 8 - bitLen);
|
||||||
if (extraBits > 0n) x = x >> extraBits;
|
if (extraBits > 0n) x = x >> extraBits;
|
||||||
|
|
||||||
if (x < maxExclusive) return x;
|
if (x < maxExclusive) return x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRandomOnce() {
|
function setRandomOnce() {
|
||||||
const span = unsignedMaxExclusive(bitCount); // 2^n
|
const span = unsignedMaxExclusive(bitCount);
|
||||||
const u = cryptoRandomBigInt(span);
|
const u = cryptoRandomBigInt(span);
|
||||||
unsignedBigIntToBits(u);
|
unsignedBigIntToBits(u);
|
||||||
updateUI();
|
updateUI();
|
||||||
@@ -337,11 +437,8 @@
|
|||||||
randomTimer = null;
|
randomTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pulse while running
|
|
||||||
btnRandom?.classList.add("is-running");
|
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const durationMs = 1125; // 25% longer than 900ms
|
const durationMs = 1125; // (your “~25% longer” vs 900ms)
|
||||||
const tickMs = 80;
|
const tickMs = 80;
|
||||||
|
|
||||||
randomTimer = setInterval(() => {
|
randomTimer = setInterval(() => {
|
||||||
@@ -349,31 +446,30 @@
|
|||||||
if (Date.now() - start >= durationMs) {
|
if (Date.now() - start >= durationMs) {
|
||||||
clearInterval(randomTimer);
|
clearInterval(randomTimer);
|
||||||
randomTimer = null;
|
randomTimer = null;
|
||||||
btnRandom?.classList.remove("is-running");
|
|
||||||
}
|
}
|
||||||
}, tickMs);
|
}, tickMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
BIT WIDTH
|
BIT WIDTH CONTROLS
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function setBitWidth(n) {
|
function setBitWidth(n) {
|
||||||
buildBits(clampInt(n, 1, 64));
|
buildBits(clampInt(n, 1, 64));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
TOOLBOX VISIBILITY
|
TOOLBOX TOGGLE (simple open/close state)
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function setToolboxVisible(isVisible) {
|
function setToolboxOpen(open) {
|
||||||
if (!toolboxPanel) return;
|
document.body.classList.toggle("toolboxClosed", !open);
|
||||||
toolboxPanel.style.display = isVisible ? "flex" : "none";
|
toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
|
refreshBinaryWrap(); // width changes when toolbox closes/opens
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
EVENTS
|
EVENTS
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
modeToggle?.addEventListener("change", updateUI);
|
modeToggle?.addEventListener("change", () => updateUI());
|
||||||
|
|
||||||
btnCustomBinary?.addEventListener("click", () => {
|
btnCustomBinary?.addEventListener("click", () => {
|
||||||
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
|
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
|
||||||
@@ -384,8 +480,8 @@
|
|||||||
btnCustomDenary?.addEventListener("click", () => {
|
btnCustomDenary?.addEventListener("click", () => {
|
||||||
const v = prompt(
|
const v = prompt(
|
||||||
isTwosMode()
|
isTwosMode()
|
||||||
? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
|
? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
|
||||||
: `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
|
: `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
|
||||||
);
|
);
|
||||||
if (v === null) return;
|
if (v === null) return;
|
||||||
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
|
||||||
@@ -406,8 +502,15 @@
|
|||||||
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
|
||||||
|
|
||||||
toolboxToggle?.addEventListener("click", () => {
|
toolboxToggle?.addEventListener("click", () => {
|
||||||
const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
|
const isOpen = !document.body.classList.contains("toolboxClosed");
|
||||||
setToolboxVisible(!isOpen);
|
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();
|
updateModeHint();
|
||||||
buildBits(bitCount);
|
buildBits(bitCount);
|
||||||
setToolboxVisible(true);
|
setToolboxOpen(true);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,412 +1,326 @@
|
|||||||
|
/* Binary page styles (moved OUT of binary.astro) */
|
||||||
|
|
||||||
:root{
|
:root{
|
||||||
--bg: #1f2027;
|
--panel-w: 360px;
|
||||||
--panel2: rgba(255,255,255,.04);
|
--gap: 22px;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap{
|
.wrap{
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 26px 22px 60px;
|
padding: 26px 20px 48px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MAIN GRID */
|
|
||||||
.topGrid{
|
.topGrid{
|
||||||
display:grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 360px;
|
grid-template-columns: 1fr var(--panel-w);
|
||||||
gap: 28px;
|
gap: var(--gap);
|
||||||
align-items:start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toolbox toggle BELOW navbar */
|
/* When toolbox is hidden, reclaim space + centre content */
|
||||||
.toolboxToggle{
|
body.toolboxClosed .topGrid{
|
||||||
position: sticky;
|
grid-template-columns: 1fr;
|
||||||
top: calc(var(--nav-height) + 10px);
|
}
|
||||||
z-index: 900;
|
body.toolboxClosed #toolboxPanel{
|
||||||
float: right;
|
display: none;
|
||||||
|
}
|
||||||
display:inline-flex;
|
|
||||||
align-items:center;
|
.mainCol{
|
||||||
gap: 10px;
|
min-width: 0;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
.toolboxToggle:hover{ background: rgba(255,255,255,.09); }
|
|
||||||
|
|
||||||
/* Readout */
|
|
||||||
.readout{
|
.readout{
|
||||||
text-align:center;
|
text-align: center;
|
||||||
padding: 10px 10px 0;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label{
|
.label{
|
||||||
letter-spacing: .18em;
|
opacity: .8;
|
||||||
font-weight: 900;
|
letter-spacing: .12em;
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
|
||||||
.num{
|
.num{
|
||||||
font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
display: inline-block;
|
||||||
font-weight: 400;
|
width: fit-content;
|
||||||
color: var(--accent);
|
max-width: 100%;
|
||||||
text-shadow: 0 0 18px var(--accent-dim);
|
white-space: pre-line; /* allows JS \n wraps */
|
||||||
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.denaryValue{
|
.denaryValue{
|
||||||
font-size: 52px;
|
font-size: 54px;
|
||||||
line-height: 1.0;
|
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.binaryValue{
|
.binaryValue{
|
||||||
font-size: 46px;
|
font-size: 56px;
|
||||||
letter-spacing: .14em;
|
margin: 4px 0 18px;
|
||||||
line-height: 1.05;
|
|
||||||
margin: 6px 0 14px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider{
|
.divider{
|
||||||
margin-top: 26px;
|
height: 1px;
|
||||||
border-top: 1px solid var(--line);
|
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{
|
.panelCol{
|
||||||
display:flex;
|
position: sticky;
|
||||||
flex-direction:column;
|
top: calc(var(--nav-h, 72px) + 18px);
|
||||||
gap: 14px;
|
align-self: start;
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
.card{
|
.card{
|
||||||
background: var(--panel2);
|
border: 1px solid rgba(255,255,255,.12);
|
||||||
border: 1px solid rgba(255,255,255,.10);
|
border-radius: 16px;
|
||||||
border-radius: 14px;
|
background: rgba(255,255,255,.05);
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardTitle{
|
.cardTitle{
|
||||||
letter-spacing: .18em;
|
opacity: .8;
|
||||||
font-weight: 900;
|
letter-spacing: .14em;
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 12px;
|
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{
|
.subCard{
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
background: rgba(255,255,255,.04);
|
|
||||||
border: 1px solid rgba(255,255,255,.10);
|
border: 1px solid rgba(255,255,255,.10);
|
||||||
border-radius: 12px;
|
border-radius: 14px;
|
||||||
|
background: rgba(0,0,0,.12);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subTitle{
|
.subTitle{
|
||||||
letter-spacing: .18em;
|
opacity: .8;
|
||||||
font-weight: 900;
|
letter-spacing: .14em;
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 11px;
|
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;
|
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{
|
.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;
|
||||||
}
|
|
||||||
|
|
||||||
.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{
|
.bitInputWrap{
|
||||||
background:rgba(255,255,255,.06);
|
display: grid;
|
||||||
border:1px solid rgba(255,255,255,.14);
|
grid-template-columns: auto 1fr;
|
||||||
border-radius:12px;
|
gap: 10px;
|
||||||
padding:10px 12px;
|
align-items: center;
|
||||||
display:flex;
|
padding: 10px 12px;
|
||||||
align-items:center;
|
border: 1px solid rgba(255,255,255,.10);
|
||||||
justify-content:space-between;
|
border-radius: 12px;
|
||||||
gap:12px;
|
background: rgba(255,255,255,.04);
|
||||||
}
|
}
|
||||||
.bitInputLabel{
|
.bitInputLabel{
|
||||||
color:var(--muted);
|
opacity: .75;
|
||||||
font-size:12px;
|
letter-spacing: .14em;
|
||||||
font-weight:900;
|
text-transform: uppercase;
|
||||||
letter-spacing:.18em;
|
font-size: 11px;
|
||||||
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 grid */
|
|
||||||
.bitsWrap{ margin-top: 22px; }
|
|
||||||
|
|
||||||
.bitsGrid{
|
|
||||||
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;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 4px;
|
|
||||||
text-align:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bulb 25% bigger */
|
|
||||||
.bulb{
|
|
||||||
font-size: 32px; /* was ~26 */
|
|
||||||
line-height: 1;
|
|
||||||
opacity: .45;
|
|
||||||
filter: grayscale(1);
|
|
||||||
}
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.bitInput{
|
||||||
@media (max-width: 980px){
|
width: 100%;
|
||||||
.topGrid{ grid-template-columns: 1fr; }
|
min-width: 0;
|
||||||
.toolboxToggle{ float:none; }
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.controlsRow{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.btnWide{ width: 100%; }
|
||||||
|
|
||||||
|
.btnAccent{
|
||||||
|
background: rgba(0,255,140,.12);
|
||||||
|
border-color: rgba(0,255,140,.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolRowCentered{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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