0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Sixteen switches, one number: flip a switch and the decimal, hex, octal and binary readouts all move together — or type into any readout and the switches rearrange to match. It opens on 0xC0DE with the running sum spelled out underneath as 32,768 + 16,384 + 128 + …, so the arithmetic behind a base is shown rather than asserted. A second panel combines that number with another one bit by bit — AND, OR, XOR, NOT and shifts on a slider — and marks exactly which switches moved. Drag the shift slider first to watch the number double or halve while the demo counts the switches falling off the edge, then run XOR twice against the same second number and land precisely where you started.
Source — the code you see is the code that just ran 916 lines 39.3 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Binary Playground — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ---- derived surfaces (theme-paired, same rules as the tokens above) ---- */
:root { --off-face: #FBFBF9; --glow: rgba(192,86,0,.15); --wash: #F1F0EC; --dim: .42;
--accent-ink: #9A4500; } /* accent used as TEXT — darkened so it clears 4.5:1 on paper */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --off-face: #181B21; --glow: rgba(255,180,84,.13); --wash: #15181D; --dim: .38;
--accent-ink: #FFB454; }
}
:root[data-theme="dark"] { --off-face: #181B21; --glow: rgba(255,180,84,.13); --wash: #15181D; --dim: .38;
--accent-ink: #FFB454; }
/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; overflow-x: hidden; -webkit-text-size-adjust: 100%; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 26px 16px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
h2 { font-size: .95rem; margin: 0; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 68ch; }
.lede b { color: var(--ink); font-weight: 600; }
.mono, code { font-family: var(--font-mono); }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
footer { margin-top: 24px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
.k-lab {
font-family: var(--font-mono); font-size: .64rem; letter-spacing: .11em;
text-transform: uppercase; color: var(--muted);
}
.panel {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px; margin-top: 16px;
}
.panelhead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: 12px; }
.panelhead .k-lab { margin-left: auto; }
.btn {
font-family: var(--font-mono); font-size: .73rem; letter-spacing: .02em;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 999px; padding: 6px 11px; min-height: 32px; cursor: pointer;
}
.btn:hover { border-color: var(--accent); color: var(--accent-ink); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink); }
.btn.ghost { background: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--accent-ink); }
.btn.ok { border-color: var(--run); color: var(--run); background: var(--run-soft); }
.btn:disabled { opacity: var(--dim); cursor: not-allowed; }
.btn:disabled:hover { border-color: var(--line); color: var(--ink); }
.presets { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
/* ---- the switch bank ---- */
.bytebar {
display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 7px;
}
.bytebar span { text-align: center; }
.bits { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 14px; }
.nib { min-width: 0; }
.nib .row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.nibfoot { display: flex; justify-content: center; margin-top: 7px; }
.hexd {
font-family: var(--font-mono); font-size: .68rem; color: var(--muted);
background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 1px 9px;
}
.hexd b { color: var(--accent-ink); font-weight: 600; }
.cell { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }
.idx { font-family: var(--font-mono); font-size: .58rem; color: var(--muted); }
.bit {
width: 100%; height: 46px; padding: 0; cursor: pointer; position: relative;
border: 1px solid var(--line); background: var(--off-face); border-radius: 9px;
color: var(--muted); display: grid; place-items: center; overflow: hidden;
}
.bit .digit { font-family: var(--font-mono); font-size: 1.1rem; font-weight: 600; line-height: 1; display: block; }
.bit::before {
content: ""; position: absolute; top: 5px; left: 50%; transform: translateX(-50%);
width: 14px; height: 3px; border-radius: 2px; background: var(--line);
}
.bit[aria-checked="true"] {
background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink);
box-shadow: 0 0 0 3px var(--glow);
}
.bit[aria-checked="true"]::before { background: var(--accent); }
.bit:hover { border-color: var(--accent); }
.bit:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bit:disabled { cursor: not-allowed; }
.bit:disabled:hover { border-color: var(--line); }
.pv { font-family: var(--font-mono); font-size: .56rem; color: var(--muted); letter-spacing: -.02em; }
.cell.on .pv { color: var(--accent-ink); }
.cell.hi .bit { border-color: var(--accent); box-shadow: 0 0 0 3px var(--glow); }
.sum {
margin: 14px 0 0; padding-top: 12px; border-top: 1px dashed var(--line);
display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
font-family: var(--font-mono); font-size: .74rem; color: var(--muted);
}
.chip {
font-family: var(--font-mono); font-size: .74rem; cursor: pointer;
background: var(--accent-soft); color: var(--accent-ink);
border: 1px solid var(--accent); border-radius: 6px; padding: 3px 8px; min-height: 26px;
}
.chip:hover { background: var(--accent); color: var(--surface); }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sum .eq { color: var(--ink); }
.sum .tot { color: var(--ink); font-weight: 600; font-size: .82rem; }
/* ---- readouts ---- */
.fields { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.field { min-width: 0; }
.field label { display: block; margin-bottom: 4px; }
.field input {
width: 100%; font-family: var(--font-mono); font-size: .9rem; color: var(--ink);
background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
padding: 9px 10px; min-height: 40px;
}
.field input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--glow); }
.field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.field.bad input { border-color: var(--danger); }
.field.bin { grid-column: 1 / -1; }
.note { margin: 10px 0 0; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); min-height: 1.1em; }
.note.bad { color: var(--danger); }
.note.good { color: var(--run); }
.facts {
list-style: none; margin: 12px 0 0; padding: 12px 0 0; border-top: 1px dashed var(--line);
display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 10px 14px;
}
.facts li { min-width: 0; }
.facts .v {
display: block; font-family: var(--font-mono); font-size: .85rem; color: var(--ink);
overflow-wrap: anywhere;
}
/* ---- combine lab ---- */
.ops { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 12px; }
.shiftbox {
display: flex; align-items: center; gap: 8px; margin-left: 4px;
background: var(--bg); border: 1px solid var(--line); border-radius: 999px; padding: 3px 12px 3px 10px;
}
.shiftbox input[type="range"] { width: 96px; accent-color: var(--accent); }
.shiftbox input[type="range"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.shiftbox .amt { font-family: var(--font-mono); font-size: .74rem; color: var(--accent-ink); min-width: 1.4em; text-align: right; }
.shiftbox[hidden] { display: none; }
.lane { margin-top: 10px; }
.lanehead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px; margin-bottom: 5px; }
.lanehead .val { font-family: var(--font-mono); font-size: .72rem; color: var(--ink); }
.lanehead #rLegend { margin-left: auto; text-align: right; }
.lane.off { opacity: var(--dim); }
.bits.mini { gap: 8px 12px; }
.bits.mini .row { gap: 4px; }
.bits.mini .bit { height: 30px; border-radius: 7px; }
.bits.mini .bit::before { display: none; }
.bits.mini .bit .digit { font-size: .84rem; }
.rcell { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }
.rcell .face {
width: 100%; height: 30px; border: 1px solid var(--line); background: var(--off-face);
border-radius: 7px; display: grid; place-items: center;
font-family: var(--font-mono); font-size: .84rem; font-weight: 600; color: var(--muted);
}
.rcell.on .face { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink); }
.rcell .mark { width: 60%; height: 2px; border-radius: 2px; background: transparent; }
.rcell.changed .mark { background: var(--accent); }
.bin-input {
display: flex; align-items: center; gap: 8px; margin-top: 8px; flex-wrap: wrap;
}
.bin-input input {
font-family: var(--font-mono); font-size: .82rem; color: var(--ink); width: 118px;
background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 7px 9px; min-height: 36px;
}
.bin-input input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--glow); }
.bin-input input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.bin-input input.bad { border-color: var(--danger); }
#bHint.bad { color: var(--danger); }
/* the whole lane is already dimmed by .lane.off — don't stack a second opacity here */
.bin-input input:disabled { cursor: not-allowed; }
.formula {
margin: 14px 0 0; padding: 10px 12px; background: var(--bg); border: 1px solid var(--line);
border-radius: 8px; font-family: var(--font-mono); font-size: .76rem; color: var(--muted);
overflow-x: auto; white-space: nowrap;
}
.formula b { color: var(--ink); font-weight: 600; }
.formula .op { color: var(--accent-ink); }
.formula .tail { margin-left: 14px; padding-left: 14px; border-left: 1px solid var(--line); }
.opnote { margin: 10px 0 0; font-size: .84rem; color: var(--muted); max-width: 68ch; }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
/* ---- motion (opt-in only) ---- */
@media (prefers-reduced-motion: no-preference) {
.btn, .chip, .bit, .rcell .face, .rcell .mark, .field input, .lane, .hexd b {
transition: background-color .14s ease, border-color .14s ease, color .14s ease,
box-shadow .14s ease, opacity .18s ease;
}
.bit.anim .digit { animation: flip .3s cubic-bezier(.3,.9,.3,1) both; }
.bit.anim { animation: ring .48s ease-out backwards; }
.rcell.anim .face { animation: pop .28s ease-out both; }
@keyframes flip {
0% { transform: rotateX(-88deg) scale(.7); opacity: .15; }
60% { transform: rotateX(12deg) scale(1.06); opacity: 1; }
100% { transform: none; opacity: 1; }
}
@keyframes ring {
0% { box-shadow: 0 0 0 0 var(--glow); }
45% { box-shadow: 0 0 0 9px var(--glow); }
100% { box-shadow: 0 0 0 3px var(--glow); }
}
@keyframes pop {
0% { transform: scale(.82); } 55% { transform: scale(1.08); } 100% { transform: none; }
}
}
/* ---- narrow ---- */
@media (max-width: 760px) {
.fields { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
.wrap { padding: 20px 12px 30px; }
.bits { grid-template-columns: repeat(2, 1fr); gap: 12px 10px; }
.bits .nib:nth-child(3) .row, .bits .nib:nth-child(4) .row { margin-top: 0; }
.bytebar { display: none; }
.bit { height: 44px; }
.pv { font-size: .5rem; }
.idx { font-size: .54rem; }
.formula { white-space: normal; }
}
</style>
</head>
<body>
<main class="wrap">
<header>
<h1>Binary Playground</h1>
<p class="lede">Sixteen switches, one number. Flip a switch and watch the <b>decimal, hex and octal</b> readouts move together — or type a number into any of them and the switches rearrange to match. It starts on <code class="mono">0xC0DE</code>, a number whose hex spells a word.</p>
</header>
<section class="panel" aria-labelledby="h-switches">
<div class="panelhead">
<h2 id="h-switches">The switches</h2>
<div class="presets" id="presets" role="group" aria-label="Preset values"></div>
</div>
<div class="bytebar k-lab" aria-hidden="true"><span>high byte</span><span>low byte</span></div>
<div class="bits" id="bankA" role="group" aria-label="Sixteen switches, from bit 15 on the left down to bit 0 on the right"></div>
<p class="sum" id="sumLine"></p>
<p class="sr-only" id="live" aria-live="polite"></p>
</section>
<section class="panel" aria-labelledby="h-read">
<div class="panelhead">
<h2 id="h-read">The same number, four ways</h2>
<button class="btn ghost" id="copyBtn" type="button">copy all</button>
</div>
<div class="fields">
<div class="field" id="f-dec"><label class="k-lab" for="in-dec">decimal</label><input id="in-dec" type="text" inputmode="numeric" autocomplete="off" spellcheck="false"></div>
<div class="field" id="f-hex"><label class="k-lab" for="in-hex">hex · base 16</label><input id="in-hex" type="text" autocomplete="off" spellcheck="false"></div>
<div class="field" id="f-oct"><label class="k-lab" for="in-oct">octal · base 8</label><input id="in-oct" type="text" inputmode="numeric" autocomplete="off" spellcheck="false"></div>
<div class="field bin" id="f-bin"><label class="k-lab" for="in-bin">binary · base 2</label><input id="in-bin" type="text" autocomplete="off" spellcheck="false"></div>
</div>
<p class="note" id="fieldNote" aria-live="polite"></p>
<ul class="facts" id="facts"></ul>
</section>
<section class="panel" aria-labelledby="h-lab">
<div class="panelhead">
<h2 id="h-lab">Combine it with a second number</h2>
<span class="k-lab">bit by bit</span>
</div>
<div class="ops" role="group" aria-label="Choose an operation" id="ops">
<button class="btn" type="button" data-op="and" aria-pressed="true">AND</button>
<button class="btn" type="button" data-op="or" aria-pressed="false">OR</button>
<button class="btn" type="button" data-op="xor" aria-pressed="false">XOR</button>
<button class="btn" type="button" data-op="not" aria-pressed="false">NOT</button>
<button class="btn" type="button" data-op="shl" aria-pressed="false"><< left</button>
<button class="btn" type="button" data-op="shr" aria-pressed="false">>> right</button>
<span class="shiftbox" id="shiftBox" hidden>
<label class="k-lab" for="shiftAmt">by</label>
<input id="shiftAmt" type="range" min="0" max="15" step="1" value="3">
<span class="amt mono" id="shiftAmtOut" aria-hidden="true">3</span>
</span>
</div>
<div class="lane">
<div class="lanehead"><span class="k-lab">first number</span><span class="val" id="valA"></span></div>
<div class="bits mini" id="bankAMirror" aria-hidden="true"></div>
</div>
<div class="lane" id="laneB">
<div class="lanehead"><span class="k-lab">second number</span><span class="val" id="valB"></span></div>
<div class="bits mini" id="bankB" role="group" aria-label="Sixteen switches for the second number"></div>
<div class="bin-input">
<label class="k-lab" for="in-b">type it</label>
<input id="in-b" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" aria-describedby="bHint">
<span class="k-lab" id="bHint">decimal, or 0x for hex</span>
</div>
</div>
<div class="lane">
<div class="lanehead"><span class="k-lab">result</span><span class="val" id="valR"></span><span class="k-lab" id="rLegend"></span></div>
<div class="bits mini" id="bankR" aria-hidden="true"></div>
</div>
<p class="formula" id="formula"></p>
<p class="opnote" id="opNote"></p>
<div class="actions">
<button class="btn" type="button" id="applyBtn">Send the result to the switches</button>
<button class="btn ghost" type="button" id="swapBtn">swap the two numbers</button>
<span class="note" id="applyNote" aria-live="polite" style="margin:0"></span>
</div>
</section>
<footer>demo · sloprun.dev</footer>
</main>
<script>
(function () {
"use strict";
var MASK = 0xFFFF, N = 16;
var $ = function (id) { return document.getElementById(id); };
var state = { a: 0xC0DE, b: 0x00FF, op: "and", shift: 3 };
var OPS = {
and: { sym: "AND", apply: function (a, b) { return a & b; },
note: "AND keeps a switch on only where both numbers have it on. That makes it a mask: anything switched off in the second number is erased from the first, so you can keep just the slice you care about." },
or: { sym: "OR", apply: function (a, b) { return a | b; },
note: "OR turns a switch on where either number has it on. Nothing that was on can go off." },
xor: { sym: "XOR", apply: function (a, b) { return a ^ b; },
note: "XOR turns a switch on where exactly one of the two has it on — so it flips the first number wherever the second one is on. Run it twice and you are back where you started." },
not: { sym: "NOT", unary: true, apply: function (a) { return ~a & MASK; },
note: "NOT flips every switch, on to off and off to on. It only needs one number, so the second row is resting." },
shl: { sym: "<<", shifty: true, apply: function (a, b, n) { return (a << n) & MASK; },
note: "Shifting left slides every switch toward the big end, which doubles the number each step. Whatever slides past the left edge is gone." },
shr: { sym: ">>", shifty: true, apply: function (a, b, n) { return (a >>> n) & MASK; },
note: "Shifting right slides every switch toward the small end, which halves the number each step and drops any remainder. Whatever slides past the right edge is gone." }
};
var PRESETS = [
{ label: "0xC0DE", v: 0xC0DE }, { label: '"Hi"', v: 0x4869 },
{ label: "255", v: 255 }, { label: "1024", v: 1024 },
{ label: "0xAAAA", v: 0xAAAA }, { label: "all on", v: 0xFFFF },
{ label: "all off", v: 0 }, { label: "shuffle", v: null }
];
function motionOK() {
return !(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
}
function bit(v, i) { return (v >>> i) & 1; }
function ones(v) { var c = 0; while (v) { v &= v - 1; c++; } return c; }
function hex(v) { var s = v.toString(16).toUpperCase(); while (s.length < 4) s = "0" + s; return s; }
function oct(v) { var s = v.toString(8); while (s.length < 6) s = "0" + s; return s; }
function bin(v, sep) {
var s = v.toString(2); while (s.length < N) s = "0" + s;
if (!sep) return s;
return s.slice(0, 4) + " " + s.slice(4, 8) + " " + s.slice(8, 12) + " " + s.slice(12);
}
function grouped(n) { return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
/* ---------- switch banks ---------- */
function buildBank(host, opts) {
var cells = [], i, n, cell, btn;
for (n = 0; n < 4; n++) {
var nib = document.createElement("div");
nib.className = "nib";
var row = document.createElement("div");
row.className = "row";
for (i = 0; i < 4; i++) {
var idx = 15 - (n * 4 + i);
if (opts.kind === "result") {
cell = document.createElement("div");
cell.className = "rcell";
var face = document.createElement("div");
face.className = "face";
face.textContent = "0";
var mark = document.createElement("div");
mark.className = "mark";
cell.appendChild(face); cell.appendChild(mark);
row.appendChild(cell);
cells.push({ i: idx, cell: cell, face: face });
} else {
cell = document.createElement("div");
cell.className = "cell";
if (opts.showIdx) {
var lab = document.createElement("span");
lab.className = "idx"; lab.textContent = idx;
cell.appendChild(lab);
}
btn = document.createElement("button");
btn.type = "button"; btn.className = "bit";
btn.setAttribute("role", "switch");
btn.setAttribute("aria-checked", "false");
btn.setAttribute("aria-label", opts.labelFor(idx));
var dg = document.createElement("span");
dg.className = "digit"; dg.textContent = "0";
btn.appendChild(dg);
cell.appendChild(btn);
if (opts.showPV) {
var pv = document.createElement("span");
pv.className = "pv"; pv.textContent = grouped(Math.pow(2, idx));
cell.appendChild(pv);
}
row.appendChild(cell);
cells.push({ i: idx, cell: cell, btn: btn, digit: dg });
}
}
nib.appendChild(row);
if (opts.hexFoot) {
var foot = document.createElement("div");
foot.className = "nibfoot";
var hd = document.createElement("span");
hd.className = "hexd";
hd.innerHTML = 'hex <b>0</b>';
foot.appendChild(hd);
nib.appendChild(foot);
cells[cells.length - 1].hexOut = hd.querySelector("b");
}
host.appendChild(nib);
}
return cells;
}
function paintBank(cells, value, prev, animate) {
var stagger = 0, moving = animate && motionOK();
cells.forEach(function (c) {
var on = bit(value, c.i) === 1;
var was = prev === null ? on : bit(prev, c.i) === 1;
var changed = on !== was;
if (c.btn) {
c.btn.setAttribute("aria-checked", on ? "true" : "false");
c.digit.textContent = on ? "1" : "0";
c.cell.classList.toggle("on", on);
c.btn.classList.remove("anim");
if (changed && moving) {
void c.btn.offsetWidth;
c.btn.style.animationDelay = (stagger * 14) + "ms";
c.digit.style.animationDelay = (stagger * 14) + "ms";
c.btn.classList.add("anim");
stagger++;
}
} else {
c.face.textContent = on ? "1" : "0";
c.cell.classList.toggle("on", on);
c.cell.classList.remove("anim");
if (changed && moving) {
void c.cell.offsetWidth;
c.cell.classList.add("anim");
}
}
if (c.hexOut) {
var start = c.i; /* lowest bit of this nibble */
c.hexOut.textContent = ((value >>> start) & 0xF).toString(16).toUpperCase();
}
});
}
function markChanged(cells, result, base) {
cells.forEach(function (c) {
c.cell.classList.toggle("changed", bit(result, c.i) !== bit(base, c.i));
});
}
function wireBank(cells, onToggle) {
var btns = cells.map(function (c) { return c.btn; });
btns.forEach(function (b, k) {
b.tabIndex = k === 0 ? 0 : -1;
b.addEventListener("click", function () { onToggle(cells[k].i); });
b.addEventListener("focus", function () {
btns.forEach(function (x) { x.tabIndex = -1; });
b.tabIndex = 0;
});
b.addEventListener("keydown", function (e) {
var t = -1;
if (e.key === "ArrowRight") t = k + 1;
else if (e.key === "ArrowLeft") t = k - 1;
else if (e.key === "ArrowDown") t = Math.min(15, k + 8);
else if (e.key === "ArrowUp") t = Math.max(0, k - 8);
else if (e.key === "Home") t = 0;
else if (e.key === "End") t = 15;
else return;
if (t < 0 || t > 15) { e.preventDefault(); return; }
e.preventDefault();
btns[t].focus();
});
});
}
var cellsA = buildBank($("bankA"), {
showIdx: true, showPV: true, hexFoot: true,
labelFor: function (i) { return "bit " + i + ", worth " + grouped(Math.pow(2, i)); }
});
var cellsMirror = buildBank($("bankAMirror"), { kind: "result" });
var cellsB = buildBank($("bankB"), {
labelFor: function (i) { return "second number, bit " + i; }
});
var cellsR = buildBank($("bankR"), { kind: "result" });
wireBank(cellsA, function (i) { setA(state.a ^ (1 << i)); });
wireBank(cellsB, function (i) { setB(state.b ^ (1 << i)); });
/* ---------- presets ---------- */
var presetBtns = [];
PRESETS.forEach(function (p) {
var b = document.createElement("button");
b.type = "button"; b.className = "btn"; b.textContent = p.label;
if (p.v !== null) b.setAttribute("aria-pressed", "false");
b.addEventListener("click", function () {
if (p.v !== null) { setA(p.v); return; }
var r = state.a;
while (r === state.a) r = Math.floor(Math.random() * 65536);
setA(r);
});
presetBtns.push({ b: b, v: p.v });
$("presets").appendChild(b);
});
/* the preset that matches what is on the switches reads as selected */
function syncPresets() {
presetBtns.forEach(function (p) {
if (p.v === null) return;
p.b.setAttribute("aria-pressed", p.v === state.a ? "true" : "false");
});
}
/* ---------- sum line ---------- */
function renderSum(v) {
var host = $("sumLine");
host.textContent = "";
var parts = [], i;
for (i = 15; i >= 0; i--) if (bit(v, i)) parts.push(i);
if (!parts.length) {
var z = document.createElement("span");
z.textContent = "every switch is off, so the number is 0 — flip one on.";
host.appendChild(z);
return;
}
parts.forEach(function (i, k) {
if (k) {
var plus = document.createElement("span");
plus.textContent = "+"; host.appendChild(plus);
}
var c = document.createElement("button");
c.type = "button"; c.className = "chip";
c.textContent = grouped(Math.pow(2, i));
c.setAttribute("aria-label", "turn bit " + i + " off, worth " + grouped(Math.pow(2, i)));
c.addEventListener("click", function () {
/* this chip is about to be destroyed by the re-render — hand the keyboard
the switch it just turned off instead of dropping focus on <body> */
var keep = document.activeElement === c;
setA(state.a & ~(1 << i));
if (keep) {
cellsA.forEach(function (x) { if (x.i === i) x.btn.focus(); });
}
});
c.addEventListener("mouseenter", function () { hiCell(i, true); });
c.addEventListener("mouseleave", function () { hiCell(i, false); });
c.addEventListener("focus", function () { hiCell(i, true); });
c.addEventListener("blur", function () { hiCell(i, false); });
host.appendChild(c);
});
var eq = document.createElement("span");
eq.className = "eq"; eq.textContent = "=";
var tot = document.createElement("span");
tot.className = "tot"; tot.textContent = grouped(v);
host.appendChild(eq); host.appendChild(tot);
}
function hiCell(i, on) {
cellsA.forEach(function (c) { if (c.i === i) c.cell.classList.toggle("hi", on); });
}
/* ---------- number fields ---------- */
var FIELDS = {
dec: { el: $("in-dec"), box: $("f-dec"), re: /^-?\d+$/, fmt: function (v) { return String(v); },
parse: function (s) { return parseInt(s, 10); }, strip: /^$/ },
hex: { el: $("in-hex"), box: $("f-hex"), re: /^-?[0-9a-f]+$/i, fmt: hex,
parse: function (s) { return parseInt(s, 16); }, strip: /^0x/i },
oct: { el: $("in-oct"), box: $("f-oct"), re: /^-?[0-7]+$/, fmt: oct,
parse: function (s) { return parseInt(s, 8); }, strip: /^0o/i },
bin: { el: $("in-bin"), box: $("f-bin"), re: /^-?[01]+$/, fmt: function (v) { return bin(v, true); },
parse: function (s) { return parseInt(s, 2); }, strip: /^0b/i }
};
var LABEL = { dec: "decimal", hex: "hex", oct: "octal", bin: "binary" };
function syncFields(except) {
Object.keys(FIELDS).forEach(function (k) {
if (k === except) return;
FIELDS[k].el.value = FIELDS[k].fmt(state.a);
FIELDS[k].box.classList.remove("bad");
});
}
function readField(k) {
var f = FIELDS[k];
var raw = f.el.value.trim().replace(/[\s_,]/g, "");
var neg = raw.charAt(0) === "-";
var s = (neg ? raw.slice(1) : raw).replace(f.strip, "");
if (raw === "" || s === "" || s === "-") { setNote("waiting for " + LABEL[k] + " digits", ""); f.box.classList.add("bad"); return; }
if (!f.re.test(s)) {
f.box.classList.add("bad");
setNote("that is not a valid " + LABEL[k] + " number — " + hint(k), "bad");
return;
}
var n = f.parse(s);
if (!isFinite(n)) { f.box.classList.add("bad"); setNote("that number is too long to read", "bad"); return; }
f.box.classList.remove("bad");
var msg = "";
if (neg) { n = (MASK + 1 - (n & MASK)) & MASK; msg = "negatives wrap around: sixteen switches only count up from 0."; }
else if (n > MASK) { n = n & MASK; msg = "over 65,535 — kept the low sixteen switches, the rest fell off the left."; }
setNote(msg, "");
setA(n, k);
}
function hint(k) {
return k === "dec" ? "digits 0 to 9 only" :
k === "hex" ? "digits 0 to 9 and letters A to F" :
k === "oct" ? "digits 0 to 7 only" : "only 0 and 1";
}
function setNote(text, cls) {
var n = $("fieldNote");
n.textContent = text;
n.className = "note" + (cls ? " " + cls : "");
}
Object.keys(FIELDS).forEach(function (k) {
FIELDS[k].el.addEventListener("input", function () { readField(k); });
FIELDS[k].el.addEventListener("blur", function () {
FIELDS[k].el.value = FIELDS[k].fmt(state.a);
FIELDS[k].box.classList.remove("bad");
setNote("", "");
});
});
/* ---------- facts ---------- */
function renderFacts(v) {
var signed = v > 32767 ? v - 65536 : v;
var hi = (v >>> 8) & 0xFF, lo = v & 0xFF;
var printable = function (c) { return c >= 32 && c <= 126; };
var text = (printable(hi) && printable(lo))
? '"' + String.fromCharCode(hi) + String.fromCharCode(lo) + '"' : "—";
var top = -1, i;
for (i = 15; i >= 0; i--) if (bit(v, i)) { top = i; break; }
var pow2 = v > 0 && (v & (v - 1)) === 0;
var items = [
["switches on", ones(v) + " of 16"],
["as two bytes", hex(v).slice(0, 2) + " · " + hex(v).slice(2)],
["read as text", text],
["if it were signed", grouped(signed)],
["biggest switch on", top < 0 ? "—" : "bit " + top + " (" + grouped(Math.pow(2, top)) + ")"],
["a doubling number", pow2 ? "yes, 2^" + top : "no"]
];
var ul = $("facts");
ul.textContent = "";
items.forEach(function (it) {
var li = document.createElement("li");
var k = document.createElement("span");
k.className = "k-lab"; k.textContent = it[0];
var val = document.createElement("span");
val.className = "v"; val.textContent = it[1];
li.appendChild(k); li.appendChild(val);
ul.appendChild(li);
});
}
/* ---------- combine lab ---------- */
function currentResult() {
var o = OPS[state.op];
return o.apply(state.a, state.b, state.shift) & MASK;
}
var shown = { a: null, b: null, r: null }; /* what each lane is currently painted with */
function renderLab(animate) {
var o = OPS[state.op], r = currentResult();
var unused = !!(o.unary || o.shifty);
/* don't strand the keyboard on a control we are about to disable or hide */
var stranded = (unused && $("laneB").contains(document.activeElement)) ||
(!o.shifty && $("shiftBox").contains(document.activeElement));
if (stranded) {
var cur = $("ops").querySelector('button[data-op][aria-pressed="true"]');
if (cur) cur.focus();
}
$("laneB").classList.toggle("off", unused);
cellsB.forEach(function (c) { c.btn.disabled = unused; });
$("in-b").disabled = unused;
if (unused) bHint(B_HINT, false); /* clear any stale typing error while it rests */
$("shiftBox").hidden = !o.shifty;
paintBank(cellsMirror, state.a, shown.a, animate);
paintBank(cellsB, state.b, shown.b, animate);
paintBank(cellsR, r, shown.r, animate);
markChanged(cellsR, r, state.a);
shown.a = state.a; shown.b = state.b; shown.r = r;
$("valA").textContent = grouped(state.a) + " · 0x" + hex(state.a);
$("valB").textContent = unused ? "not used by " + o.sym : grouped(state.b) + " · 0x" + hex(state.b);
$("valR").textContent = grouped(r) + " · 0x" + hex(r);
var moved = ones(r ^ state.a);
$("rLegend").textContent = moved
? (moved === 1 ? "1 underlined switch moved" : moved + " underlined switches moved")
: "same as the first number";
var f = $("formula");
f.textContent = "";
var add = function (t, cls) {
var s = document.createElement(cls === "b" ? "b" : "span");
if (cls && cls !== "b") s.className = cls;
s.textContent = t; f.appendChild(s);
};
if (o.unary) {
add(o.sym + " ", "op"); add(bin(state.a, true), "b");
add(" = "); add(bin(r, true), "b");
} else if (o.shifty) {
add(bin(state.a, true), "b"); add(" " + o.sym + " " + state.shift + " ", "op");
add("= "); add(bin(r, true), "b");
} else {
add(bin(state.a, true), "b"); add(" " + o.sym + " ", "op");
add(bin(state.b, true), "b"); add(" = "); add(bin(r, true), "b");
}
add(o.unary
? o.sym + " " + grouped(state.a) + " = " + grouped(r)
: grouped(state.a) + " " + o.sym + " " + (o.shifty ? state.shift : grouped(state.b)) + " = " + grouped(r), "tail");
var note = o.note;
if (o.shifty && state.shift === 0) {
note += " At a distance of zero the number is left exactly as it is — drag the slider.";
} else if (o.shifty) {
var lost = o.sym === "<<"
? ones(state.a & ~(MASK >>> state.shift))
: ones(state.a & ~((MASK << state.shift) & MASK));
note += lost ? " Right now " + lost + (lost === 1 ? " switch falls" : " switches fall") + " off the edge." :
" Nothing falls off the edge at this distance.";
}
$("opNote").textContent = note;
}
Array.prototype.forEach.call($("ops").querySelectorAll("button[data-op]"), function (b) {
b.addEventListener("click", function () {
state.op = b.getAttribute("data-op");
Array.prototype.forEach.call($("ops").querySelectorAll("button[data-op]"), function (x) {
x.setAttribute("aria-pressed", x === b ? "true" : "false");
});
renderLab(true);
flash("");
});
});
$("shiftAmt").addEventListener("input", function () {
state.shift = parseInt(this.value, 10) || 0;
$("shiftAmtOut").textContent = state.shift;
this.setAttribute("aria-label", "shift distance, " + state.shift + " places");
renderLab(true);
});
var B_HINT = "decimal, or 0x for hex";
function bHint(text, bad) {
var h = $("bHint");
h.textContent = text;
h.className = "k-lab" + (bad ? " bad" : "");
$("in-b").classList.toggle("bad", !!bad);
}
$("in-b").addEventListener("input", function () {
var raw = this.value.trim().replace(/[\s_,]/g, ""), v;
if (raw === "") { bHint("waiting for a second number", true); return; }
if (/^0x[0-9a-f]+$/i.test(raw)) v = parseInt(raw.slice(2), 16);
else if (/^0b[01]+$/i.test(raw)) v = parseInt(raw.slice(2), 2);
else if (/^\d+$/.test(raw)) v = parseInt(raw, 10);
else { bHint("that is not a number I can read — " + B_HINT, true); return; }
if (!isFinite(v)) { bHint("that number is too long to read", true); return; }
bHint(v > MASK ? "over 65,535 — kept the low sixteen switches" : B_HINT, false);
setB(v & MASK, true);
});
$("in-b").addEventListener("blur", function () {
this.value = String(state.b);
bHint(B_HINT, false);
});
$("applyBtn").addEventListener("click", function () {
var r = currentResult();
if (r === state.a) { flash("the result already matches the switches", ""); return; }
setA(r);
flash("done — the switches now hold " + grouped(r), "good");
});
$("swapBtn").addEventListener("click", function () {
var a = state.a, b = state.b;
state.b = a;
$("in-b").value = String(a);
setA(b);
flash("swapped — " + grouped(b) + " is now on the switches");
});
var flashTimer = null;
function flash(text, cls) {
var n = $("applyNote");
n.textContent = text;
n.className = "note" + (cls ? " " + cls : "");
if (flashTimer) window.clearTimeout(flashTimer);
if (text) flashTimer = window.setTimeout(function () { n.textContent = ""; n.className = "note"; }, 2600);
}
/* ---------- copy ---------- */
$("copyBtn").addEventListener("click", function () {
var btn = this;
var text = "dec " + state.a + "\nhex 0x" + hex(state.a) + "\noct 0o" + oct(state.a) + "\nbin " + bin(state.a, true);
var done = function (okay) {
btn.textContent = okay ? "copied" : "copy blocked here";
btn.classList.toggle("ok", okay);
window.setTimeout(function () { btn.textContent = "copy all"; btn.classList.remove("ok"); }, 1800);
};
var fallback = function () {
try {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
var okay = document.execCommand && document.execCommand("copy");
document.body.removeChild(ta);
done(!!okay);
} catch (e) { done(false); }
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { done(true); }, fallback);
} else fallback();
});
/* ---------- state setters ---------- */
var liveTimer = null;
function setA(v, fromField) {
v = v & MASK;
var prev = state.a;
/* No change in value, but a sibling field may still be showing the raw text
the visitor typed a moment ago — renormalize those and stop. */
if (v === prev && fromField) { syncFields(fromField); return; }
state.a = v;
paintBank(cellsA, v, prev, true);
renderSum(v);
renderFacts(v);
syncFields(fromField);
syncPresets();
renderLab(true);
if (liveTimer) window.clearTimeout(liveTimer);
liveTimer = window.setTimeout(function () {
$("live").textContent = "Value " + grouped(state.a) + ", hex " + hex(state.a) +
", binary " + bin(state.a, true) + ".";
}, 350);
}
function setB(v, fromField) {
v = v & MASK;
state.b = v;
if (!fromField) $("in-b").value = String(v);
renderLab(true);
}
/* ---------- platform theme ---------- */
window.addEventListener("message", function (e) {
var d = e && e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
/* ---------- boot ---------- */
$("shiftAmt").value = state.shift;
$("shiftAmtOut").textContent = state.shift;
$("shiftAmt").setAttribute("aria-label", "shift distance, " + state.shift + " places");
$("in-b").value = String(state.b);
paintBank(cellsA, state.a, null, false);
renderSum(state.a);
renderFacts(state.a);
syncFields(null);
syncPresets();
renderLab(false);
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.