<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>21-bit Converter — tunable subtract, shows its work</title>
<style>
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial; margin: 24px; color:#111; }
h1 { margin-bottom: 8px; font-size: 20px; }
.card { border:1px solid #e1e4e8; border-radius:8px; padding:16px; max-width:900px; background:#fff; }
label { display:block; margin-bottom:6px; font-weight:600; }
input[type="text"], input[type="number"] { padding:10px 12px; font-size:16px; border:1px solid #cbd5e1; border-radius:6px; box-sizing:border-box; }
input#bits { width:100%; }
button { margin-top:12px; padding:8px 12px; font-size:15px; border-radius:6px; border:0; background:#0366d6; color:#fff; cursor:pointer; }
button.secondary { background:#6b7280; }
.error { color:#b91c1c; margin-top:8px; }
pre { background:#f6f8fa; padding:12px; border-radius:6px; overflow:auto; font-size:13px; line-height:1.4; }
.steps { margin-top:12px; }
.result { margin-top:10px; font-weight:700; }
.small { font-size:13px; color:#374151; }
.row { display:flex; gap:8px; align-items:center; margin-top:8px; flex-wrap:wrap; }
.ok { color:#15803d; } .bad { color:#b91c1c; }
</style>
</head>
<body>
<div class="card">
<h1>21-bit converter — tunable subtract, shows its work</h1>
<div class="small">Start at 2 or 3 (bit 0), then ×2 (bit 0) or ×2−1 (bit 1) for the other 20 bits, then subtract the amount below.
The value before subtracting always lands in [1,048,577 , 3,145,728].</div>
<div class="row" style="margin-top:12px;">
<div style="flex:1; min-width:260px;">
<label for="bits">Bits (exactly 21)</label>
<input id="bits" type="text" placeholder="e.g. 010101001110000111000" maxlength="21" />
</div>
<div style="width:200px;">
<label for="subtract">Subtract at end</label>
<input id="subtract" type="number" value="1572864" style="width:200px;" />
</div>
</div>
<div class="small" style="margin-top:4px;">Default 1,572,864 = 50% more than 1,048,576 (2²⁰). Change it to explore.</div>
<div class="row">
<button id="compute">Compute (show work)</button>
<button id="random" class="secondary">Random</button>
<button id="example" class="secondary">Fill Example</button>
</div>
<div style="margin-top:14px;">
<label for="count">Run random trials (count)</label>
<div class="row">
<input id="count" type="number" min="1" max="2000000" value="100000" style="width:140px;" />
<button id="runTests">Run trials</button>
</div>
</div>
<div id="error" class="error" aria-live="polite"></div>
<div id="output" class="steps" aria-live="polite"></div>
</div>
<script>
const $ = id => document.getElementById(id);
const bitsInput=$('bits'), errorEl=$('error'), outputEl=$('output'), countInput=$('count'), subInput=$('subtract');
function validateBits(s){ if(!s) return 'Input is empty'; if(s.length!==21) return 'Input must be exactly 21 bits'; if(!/^[01]{21}$/.test(s)) return 'Input must contain only 0 and 1'; return ''; }
function getSub(){ const n = Number(subInput.value); return Number.isFinite(n) ? Math.round(n) : 1572864; }
// Forward with a step trace. Values stay under 2^22, exact in Number.
function forward(bitString, SUB){
const bits = bitString.split('').map(b => b==='1'?1:0);
let value = bits[0]===0 ? 2 : 3;
const trace = [{ step:0, bit:bits[0], op:'start', value }];
for(let i=1;i<21;i++){
if(bits[i]===0){ value = value*2; trace.push({step:i, bit:0, op:'×2', value}); }
else { value = value*2-1; trace.push({step:i, bit:1, op:'×2 − 1', value}); }
}
return { before: value, after: value - SUB, trace };
}
function forwardBefore(s){ let v=s[0]==='0'?2:3; for(let i=1;i<21;i++){ v = s[i]==='0'? v*2 : v*2-1; } return v; }
// Disambiguation bits + correct variable-length reverse (independent of the subtract amount).
function disambigBits(s){ let v=forwardBefore(s); const dis=[]; for(let step=0;step<20;step++){ if(v===3) dis.push('1'); v=(v%2===0)?v/2:(v+1)/2; } if(v===3) dis.push('0'); return dis.join(''); }
function decode(Vbefore, dis){ let v=Vbefore, out=[], di=0, steps=0, start=null;
while(steps<200){ if(v===2){start='0';break;} if(v===3){ const d=dis[di++]; if(d==='0'){start='1';break;} }
if(v%2===0){out.push('0'); v=v/2;} else {out.push('1'); v=(v+1)/2;} steps++; }
if(start===null) return null; return start + out.reverse().join(''); }
// Reverse WITHOUT disambiguation: greedily stop at the first valid start (2 or 3). Wrong when the path passed through 3.
function revNaive(Vbefore){ let v=Vbefore, out=[], start=null, steps=0;
while(steps<200){ if(v===2){start='0';break;} if(v===3){start='1';break;} if(v%2===0){out.push('0'); v=v/2;} else {out.push('1'); v=(v+1)/2;} steps++; }
if(start===null) return null; return start + out.reverse().join(''); }
function bitLen(k){ k=Math.abs(k); return k===0?1:Math.floor(Math.log2(k))+1; }
$('compute').addEventListener('click', () => {
errorEl.textContent=''; outputEl.innerHTML='';
const s = bitsInput.value.trim(); const v = validateBits(s);
if(v){ errorEl.textContent=v; return; }
const SUB = getSub();
const f = forward(s, SUB);
const dis = disambigBits(s);
const before = forwardBefore(s);
const recWith = decode(before, dis); const matchWith = recWith===s;
const recNaive = revNaive(before); const matchNaive = recNaive===s;
let html = '<div class="small"><strong>Work</strong></div><pre>';
html += 'Step Bit Operation Value\n';
html += '---- --- --------- ----------------\n';
f.trace.forEach(t => { html += String(t.step).padEnd(4)+' '+String(t.bit).padEnd(3)+' '+String(t.op).padEnd(9)+' '+t.value+'\n'; });
html += '</pre>';
html += `<div class="result">Value before subtracting: ${f.before}</div>`;
html += `<div class="result">Subtract: ${SUB}</div>`;
html += `<div class="result">Final output: ${f.after} <span class="small">(${bitLen(f.after)} bits${f.after<0?' + sign':''})</span></div>`;
html += `<div class="result">Reverse WITHOUT the bit (stop at first 2 or 3): <span class="${matchNaive?'ok':'bad'}">${recNaive}</span> ` +
`<span class="small">${matchNaive?'matches — this input was already unambiguous':'does NOT match — a disambiguation bit is needed here'}</span></div>`;
html += `<div class="result">Reverse WITH the bit: <span class="${matchWith?'ok':'bad'}">${recWith}</span> ` +
`<span class="small">${matchWith?'matches':'FAILS'} · disambiguation bit used: ${dis===''?'none needed':dis}</span></div>`;
outputEl.innerHTML = html;
});
$('example').addEventListener('click', () => { bitsInput.value='010101001110000111000'; errorEl.textContent=''; outputEl.innerHTML=''; });
function randomBits21(){ let s=''; for(let i=0;i<21;i++) s += (Math.random()<0.5?'0':'1'); return s; }
$('random').addEventListener('click', () => { bitsInput.value=randomBits21(); $('compute').click(); });
$('runTests').addEventListener('click', () => {
errorEl.textContent=''; outputEl.innerHTML='';
const count = Math.max(1, Math.min(2000000, Number(countInput.value)||100000));
const SUB = getSub();
let min=Infinity, max=-Infinity, sum=0, sumBits=0, neg=0, correct=0; const bitDist={};
for(let i=0;i<count;i++){
const s = randomBits21();
const before = forwardBefore(s);
const after = before - SUB;
if(after<min)min=after; if(after>max)max=after; sum+=after;
if(after<0)neg++;
const bl=bitLen(after); sumBits+=bl; bitDist[bl]=(bitDist[bl]||0)+1;
if(decode(before, disambigBits(s))===s) correct++;
}
let html='<div class="small"><strong>Trial statistics (subtract = '+SUB+')</strong></div><pre>';
html += `Trials: ${count}\n`;
html += `Final output min: ${min}\n`;
html += `Final output max: ${max}\n`;
html += `Final output average: ${Math.round(sum/count)}\n`;
html += `Negative outputs: ${neg} (${(100*neg/count).toFixed(2)}%)\n`;
html += `Avg bits of |output|: ${(sumBits/count).toFixed(4)} (magnitude only, sign not counted)\n`;
html += `Reverse correct: ${correct} (${(100*correct/count).toFixed(2)}%) — subtract does not affect reversibility\n`;
html += `|output| bit-length distribution:\n ${Object.keys(bitDist).sort((a,b)=>a-b).map(k=>k+':'+bitDist[k]).join(' ')}\n`;
html += '</pre>';
html += `<div class="small">Subtracting more shifts the outputs down (some go negative), which shrinks the magnitude and its bit-count for many inputs — but the sign is one more bit, and the full range still spans 2²¹ values.</div>`;
outputEl.innerHTML = html;
});
bitsInput.addEventListener('keydown', e => { if(e.key==='Enter') $('compute').click(); });
</script>
</body>
</html>