<div class="controls">
<label>Message (use A, B, C only):
<input id="msg" type="text" value="AABAC" maxlength="12" spellcheck="false" autocomplete="off">
</label>
<div class="btns">
<button id="encBtn" type="button">Encode</button>
<button id="decBtn" type="button" disabled>Decode</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="freqBox" class="freq-box"></div>
<div id="log" class="log"></div>
<div id="summary" class="summary"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
label { display: block; font-weight: 600; margin-bottom: .4rem; }
input { font: 600 15px ui-monospace, monospace; padding: .35rem .6rem; border: 1px solid #adb1b8;
border-radius: 7px; width: 200px; letter-spacing: .08em; text-transform: uppercase; }
.btns { display: flex; gap: .45rem; margin-top: .55rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.freq-box { display: flex; gap: .6rem; margin: .7rem 0 .5rem; flex-wrap: wrap; }
.freq-chip { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 7px;
padding: .25rem .6rem; font: 600 13px ui-monospace, monospace; color: #1d3557; }
.log { font: 13px ui-monospace, monospace; background: #f5f7fa; border: 1px solid #d6dde3;
border-radius: 8px; padding: .6rem .8rem; max-height: 220px; overflow-y: auto;
display: none; line-height: 1.6; }
.log.visible { display: block; }
.log .step { color: #444; }
.log .step span { color: #1d3557; font-weight: 700; }
.log .step .sym { color: #c92f3c; }
.log .step .cost { color: #0a7d33; font-size: .92em; }
.log .sep { border: none; border-top: 1px dashed #cdd9e3; margin: .35rem 0; }
.summary { margin-top: .6rem; font-size: .92rem; font-weight: 600; min-height: 1.4em; }
.summary.ok { color: #0a7d33; }
.summary.bad { color: #c92f3c; }
.controls { margin-bottom: .3rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified rANS (Range Asymmetric Numeral Systems) demo.
// Alphabet: A, B, C with frequencies 4, 2, 1 (sum M=7).
// Encoding: x' = floor(x / fs) * M + (x mod fs) + CUM[s]
// Decoding: s = symbol_at(x mod M), x = fs * floor(x/M) + (x mod M) - CUM[s]
// Renormalisation keeps x in [M, 2M-1] before each encode step
// (we flush one bit when needed so x stays manageable for the demo).
const SYMS = ['A','B','C'];
const FREQ = { A: 4, B: 2, C: 1 };
const M = 7; // total frequency
const CUM = { A: 0, B: 4, C: 6 }; // cumulative frequencies
// Build decode table: for each remainder r in [0,M), which symbol?
// r in [CUM[s], CUM[s]+FREQ[s]) => symbol s
function symAt(r) {
for (const s of SYMS) if (r >= CUM[s] && r < CUM[s] + FREQ[s]) return s;
return null;
}
let encodedState = null;
let encodedBits = [];
let originalMsg = '';
function showFreq(msg) {
const box = document.getElementById('freqBox');
const counts = { A: 0, B: 0, C: 0 };
for (const ch of msg.toUpperCase()) if (ch in counts) counts[ch]++;
const total = Object.values(counts).reduce((a,b)=>a+b, 0) || 1;
box.innerHTML = SYMS.map(s => {
const p = (counts[s] / total * 100).toFixed(0);
const ideal = counts[s] > 0 ? (Math.log2(total/counts[s])).toFixed(2) : '—';
return fmt('<span class="freq-chip">%s%: %v%/%total% · ideal %ideal% bits</span>', { s: s, v: counts[s], total: total, ideal: ideal });
}).join('');
}
function encode() {
const raw = document.getElementById('msg').value.toUpperCase().replace(/[^ABC]/g,'');
if (!raw) { setSummary('Use only A, B, C.', 'bad'); return; }
originalMsg = raw;
showFreq(raw);
const log = document.getElementById('log');
log.innerHTML = '';
log.classList.add('visible');
let x = M; // initial state in renorm range
const bits = []; // flushed bits (LSB first)
addLog(log, fmt('<div class="step">Initial state: <span>%x%</span></div>', { x: x }));
addLog(log, `<hr class="sep">`);
for (const sym of raw) {
const fs = FREQ[sym];
// Renormalise: if x >= fs*2, flush bits until x < fs*2
while (x >= fs * 2) {
bits.push(x & 1);
x >>= 1;
}
const xBefore = x;
// ANS encode step
x = Math.floor(x / fs) * M + (x % fs) + CUM[sym];
const bitsUsed = Math.log2(x / xBefore).toFixed(3);
addLog(log, fmt('<div class="step">Encode <span class="sym">%sym%</span> (f=%fs%/7): <span>%xBefore%</span> → <span>%x%</span> <span class="cost">(~%bitsUsed% bits)</span></div>', { sym: sym, fs: fs, xBefore: xBefore, x: x, bitsUsed: bitsUsed }));
}
addLog(log, `<hr class="sep">`);
addLog(log, fmt('<div class="step">Final state: <span>%x%</span> | Flushed bits: <span>%length%</span></div>', { x: x, length: bits.length }));
const totalBits = Math.ceil(Math.log2(x + 1)) + bits.length;
const idealBits = raw.split('').reduce((acc, s) => acc + Math.log2(M / FREQ[s]), 0).toFixed(2);
addLog(log, fmt('<div class="step">Total bits ≈ <span>%totalBits%</span> | Ideal (entropy) ≈ <span>%idealBits%</span></div>', { totalBits: totalBits, idealBits: idealBits }));
encodedState = x;
encodedBits = bits;
setSummary(fmt('✓ Encoded ""%raw%"" → state=%x%. Press Decode to verify.', { raw: raw, x: x }), 'ok');
document.getElementById('decBtn').disabled = false;
}
function decode() {
if (encodedState === null) return;
let x = encodedState;
const bits = [...encodedBits]; // copy, we'll pop from end (it was pushed LSB-first)
const decoded = [];
const log = document.getElementById('log');
log.innerHTML = '';
log.classList.add('visible');
addLog(log, fmt('<div class="step">Starting decode from state: <span>%x%</span></div>', { x: x }));
addLog(log, `<hr class="sep">`);
for (let i = 0; i < originalMsg.length; i++) {
const r = x % M;
const sym = symAt(r);
const fs = FREQ[sym];
const xBefore = x;
// ANS decode step (inverse of encode)
x = fs * Math.floor(x / M) + r - CUM[sym];
addLog(log, fmt('<div class="step">Decode step %v%: r=<span>%r%</span> → <span class="sym">%sym%</span>, state <span>%xBefore%</span> → <span>%x%</span></div>', { v: i+1, r: r, sym: sym, xBefore: xBefore, x: x }));
decoded.push(sym);
// Renorm: restore bits that were flushed (reverse order)
while (x < M && bits.length > 0) {
x = (x << 1) | bits.pop();
}
}
addLog(log, `<hr class="sep">`);
const result = decoded.reverse().join('');
addLog(log, fmt('<div class="step">Decoded (reversed): <span>%result%</span></div>', { result: result }));
if (result === originalMsg) {
setSummary(fmt('✓ Perfect round-trip: ""%result%"" matches the original.', { result: result }), 'ok');
console.log('ANS round-trip OK:', result);
} else {
setSummary(fmt('✗ Mismatch: got ""%result%"", expected ""%originalMsg%"".', { result: result, originalMsg: originalMsg }), 'bad');
console.error('ANS round-trip FAIL:', result, '!=', originalMsg);
}
}
function addLog(container, html) {
const div = document.createElement('div');
div.innerHTML = html;
container.appendChild(div.firstChild);
}
function setSummary(text, cls) {
const s = document.getElementById('summary');
s.textContent = text; s.className = 'summary ' + cls;
}
function reset() {
encodedState = null; encodedBits = []; originalMsg = '';
document.getElementById('log').classList.remove('visible');
document.getElementById('log').innerHTML = '';
document.getElementById('freqBox').innerHTML = '';
document.getElementById('decBtn').disabled = true;
setSummary('', '');
}
document.getElementById('encBtn').addEventListener('click', encode);
document.getElementById('decBtn').addEventListener('click', decode);
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('msg').addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^ABC]/g,'');
});
showFreq('AABAC');
<div class="controls">
<label>Message (use A, B, C only):
<input id="msg" type="text" value="AABAC" maxlength="12" spellcheck="false" autocomplete="off">
</label>
<div class="btns">
<button id="encBtn" type="button">Encode</button>
<button id="decBtn" type="button" disabled>Decode</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="freqBox" class="freq-box"></div>
<div id="log" class="log"></div>
<div id="summary" class="summary"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
label { display: block; font-weight: 600; margin-bottom: .4rem; }
input { font: 600 15px ui-monospace, monospace; padding: .35rem .6rem; border: 1px solid #adb1b8;
border-radius: 7px; width: 200px; letter-spacing: .08em; text-transform: uppercase; }
.btns { display: flex; gap: .45rem; margin-top: .55rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.freq-box { display: flex; gap: .6rem; margin: .7rem 0 .5rem; flex-wrap: wrap; }
.freq-chip { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 7px;
padding: .25rem .6rem; font: 600 13px ui-monospace, monospace; color: #1d3557; }
.log { font: 13px ui-monospace, monospace; background: #f5f7fa; border: 1px solid #d6dde3;
border-radius: 8px; padding: .6rem .8rem; max-height: 220px; overflow-y: auto;
display: none; line-height: 1.6; }
.log.visible { display: block; }
.log .step { color: #444; }
.log .step span { color: #1d3557; font-weight: 700; }
.log .step .sym { color: #c92f3c; }
.log .step .cost { color: #0a7d33; font-size: .92em; }
.log .sep { border: none; border-top: 1px dashed #cdd9e3; margin: .35rem 0; }
.summary { margin-top: .6rem; font-size: .92rem; font-weight: 600; min-height: 1.4em; }
.summary.ok { color: #0a7d33; }
.summary.bad { color: #c92f3c; }
.controls { margin-bottom: .3rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified rANS (Range Asymmetric Numeral Systems) demo.
// Alphabet: A, B, C with frequencies 4, 2, 1 (sum M=7).
// Encoding: x' = floor(x / fs) * M + (x mod fs) + CUM[s]
// Decoding: s = symbol_at(x mod M), x = fs * floor(x/M) + (x mod M) - CUM[s]
// Renormalisation keeps x in [M, 2M-1] before each encode step
// (we flush one bit when needed so x stays manageable for the demo).
const SYMS = ['A','B','C'];
const FREQ = { A: 4, B: 2, C: 1 };
const M = 7; // total frequency
const CUM = { A: 0, B: 4, C: 6 }; // cumulative frequencies
// Build decode table: for each remainder r in [0,M), which symbol?
// r in [CUM[s], CUM[s]+FREQ[s]) => symbol s
function symAt(r) {
for (const s of SYMS) if (r >= CUM[s] && r < CUM[s] + FREQ[s]) return s;
return null;
}
let encodedState = null;
let encodedBits = [];
let originalMsg = '';
function showFreq(msg) {
const box = document.getElementById('freqBox');
const counts = { A: 0, B: 0, C: 0 };
for (const ch of msg.toUpperCase()) if (ch in counts) counts[ch]++;
const total = Object.values(counts).reduce((a,b)=>a+b, 0) || 1;
box.innerHTML = SYMS.map(s => {
const p = (counts[s] / total * 100).toFixed(0);
const ideal = counts[s] > 0 ? (Math.log2(total/counts[s])).toFixed(2) : '—';
return fmt('<span class="freq-chip">%s%: %v%/%total% · ideal %ideal% bits</span>', { s: s, v: counts[s], total: total, ideal: ideal });
}).join('');
}
function encode() {
const raw = document.getElementById('msg').value.toUpperCase().replace(/[^ABC]/g,'');
if (!raw) { setSummary('Use only A, B, C.', 'bad'); return; }
originalMsg = raw;
showFreq(raw);
const log = document.getElementById('log');
log.innerHTML = '';
log.classList.add('visible');
let x = M; // initial state in renorm range
const bits = []; // flushed bits (LSB first)
addLog(log, fmt('<div class="step">Initial state: <span>%x%</span></div>', { x: x }));
addLog(log, `<hr class="sep">`);
for (const sym of raw) {
const fs = FREQ[sym];
// Renormalise: if x >= fs*2, flush bits until x < fs*2
while (x >= fs * 2) {
bits.push(x & 1);
x >>= 1;
}
const xBefore = x;
// ANS encode step
x = Math.floor(x / fs) * M + (x % fs) + CUM[sym];
const bitsUsed = Math.log2(x / xBefore).toFixed(3);
addLog(log, fmt('<div class="step">Encode <span class="sym">%sym%</span> (f=%fs%/7): <span>%xBefore%</span> → <span>%x%</span> <span class="cost">(~%bitsUsed% bits)</span></div>', { sym: sym, fs: fs, xBefore: xBefore, x: x, bitsUsed: bitsUsed }));
}
addLog(log, `<hr class="sep">`);
addLog(log, fmt('<div class="step">Final state: <span>%x%</span> | Flushed bits: <span>%length%</span></div>', { x: x, length: bits.length }));
const totalBits = Math.ceil(Math.log2(x + 1)) + bits.length;
const idealBits = raw.split('').reduce((acc, s) => acc + Math.log2(M / FREQ[s]), 0).toFixed(2);
addLog(log, fmt('<div class="step">Total bits ≈ <span>%totalBits%</span> | Ideal (entropy) ≈ <span>%idealBits%</span></div>', { totalBits: totalBits, idealBits: idealBits }));
encodedState = x;
encodedBits = bits;
setSummary(fmt('✓ Encoded ""%raw%"" → state=%x%. Press Decode to verify.', { raw: raw, x: x }), 'ok');
document.getElementById('decBtn').disabled = false;
}
function decode() {
if (encodedState === null) return;
let x = encodedState;
const bits = [...encodedBits]; // copy, we'll pop from end (it was pushed LSB-first)
const decoded = [];
const log = document.getElementById('log');
log.innerHTML = '';
log.classList.add('visible');
addLog(log, fmt('<div class="step">Starting decode from state: <span>%x%</span></div>', { x: x }));
addLog(log, `<hr class="sep">`);
for (let i = 0; i < originalMsg.length; i++) {
const r = x % M;
const sym = symAt(r);
const fs = FREQ[sym];
const xBefore = x;
// ANS decode step (inverse of encode)
x = fs * Math.floor(x / M) + r - CUM[sym];
addLog(log, fmt('<div class="step">Decode step %v%: r=<span>%r%</span> → <span class="sym">%sym%</span>, state <span>%xBefore%</span> → <span>%x%</span></div>', { v: i+1, r: r, sym: sym, xBefore: xBefore, x: x }));
decoded.push(sym);
// Renorm: restore bits that were flushed (reverse order)
while (x < M && bits.length > 0) {
x = (x << 1) | bits.pop();
}
}
addLog(log, `<hr class="sep">`);
const result = decoded.reverse().join('');
addLog(log, fmt('<div class="step">Decoded (reversed): <span>%result%</span></div>', { result: result }));
if (result === originalMsg) {
setSummary(fmt('✓ Perfect round-trip: ""%result%"" matches the original.', { result: result }), 'ok');
console.log('ANS round-trip OK:', result);
} else {
setSummary(fmt('✗ Mismatch: got ""%result%"", expected ""%originalMsg%"".', { result: result, originalMsg: originalMsg }), 'bad');
console.error('ANS round-trip FAIL:', result, '!=', originalMsg);
}
}
function addLog(container, html) {
const div = document.createElement('div');
div.innerHTML = html;
container.appendChild(div.firstChild);
}
function setSummary(text, cls) {
const s = document.getElementById('summary');
s.textContent = text; s.className = 'summary ' + cls;
}
function reset() {
encodedState = null; encodedBits = []; originalMsg = '';
document.getElementById('log').classList.remove('visible');
document.getElementById('log').innerHTML = '';
document.getElementById('freqBox').innerHTML = '';
document.getElementById('decBtn').disabled = true;
setSummary('', '');
}
document.getElementById('encBtn').addEventListener('click', encode);
document.getElementById('decBtn').addEventListener('click', decode);
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('msg').addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^ABC]/g,'');
});
showFreq('AABAC');
Notice three things. First, the state grows multiplicatively — it never just shifts bits. Second, high-frequency symbols (A is most common) cause smaller jumps than rare ones (C), matching the Shannon cost −log2(p). Third, decoding is just the inverse arithmetic, perfectly reversible with no wasted bits. This is what separates ANS from Huffman: there are no rounding gaps.
Comments
Loading comments...