Below is a short repetitive text. Pick a compression strategy and watch how many bits per character it needs to store the whole thing losslessly — smaller is better.
<p class="hint">Same short text, three ways to store it. Click each button and watch the bits-per-character needed for a lossless copy fall toward the text's own entropy floor.</p>
<div class="textbox" id="textbox"></div>
<div class="btns">
<button id="raw" type="button">Raw (8 bits/char)</button>
<button id="freq" type="button">Frequency-based</button>
<button id="ctx" type="button">Order-1 model</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="meter">
<div class="meter-row">
<span class="meter-label">Bits per character:</span>
<span class="meter-value" id="bpc">8.000</span>
</div>
<div class="bar-track"><div class="bar-fill" id="barFill"></div></div>
<div class="meter-row small">
<span>Total: <b id="totalBits">448</b> bits</span>
<span>Entropy floor: <b id="floorVal">–</b></span>
</div>
</div>
<div class="status" id="status">Pick a strategy to compress the text above.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.textbox { font: 600 15px ui-monospace, monospace; background: #f3f4f6; border: 1px solid #d9dce1;
border-radius: 8px; padding: .6rem .7rem; line-height: 1.6; word-break: break-word;
margin-bottom: .6rem; }
.textbox .ch { padding: 0 1px; border-radius: 2px; transition: background .25s, color .25s; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #6d28d9;
background: #6d28d9; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #6d28d9; }
.meter { background: #fafafa; border: 1px solid #e5e7eb; border-radius: 8px; padding: .6rem .7rem; }
.meter-row { display: flex; justify-content: space-between; font-size: .9rem; margin-bottom: .3rem; }
.meter-row.small { font-size: .78rem; color: #555; margin-top: .35rem; margin-bottom: 0; }
.meter-label { font-weight: 600; }
.meter-value { font: 700 1.1rem ui-monospace, monospace; color: #6d28d9; }
.bar-track { height: 14px; background: #e5e7eb; border-radius: 7px; overflow: hidden; }
.bar-fill { height: 100%; width: 100%; background: linear-gradient(90deg,#6d28d9,#a855f7);
border-radius: 7px; transition: width .5s ease; }
.status { font-size: .9rem; font-weight: 600; margin: .6rem 0 0; min-height: 1.3em; color: #444; }
.status.ok { color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A short, repetitive demo text — real Hutter Prize entries work on enwik9, a 10^9-byte slice of Wikipedia, but the same three ideas apply.
const TEXT = "the cat sat on the mat the cat ran and the cat sat again";
// Order-0 (frequency) entropy: treats each character independently, weighted by how often it appears — the theoretical limit for any code that ignores context (Shannon, Huffman).
function order0BitsPerChar(text) {
const freq = {};
for (const ch of text) freq[ch] = (freq[ch] || 0) + 1;
const n = text.length;
let H = 0;
for (const ch in freq) {
const p = freq[ch] / n;
H -= p * Math.log2(p);
}
return H;
}
// Order-1 (context) entropy: predicts each character from the single character before it, capturing letter-pair patterns like "th" and "at" — a tiny cousin of the context models real compressors use.
function order1BitsPerChar(text) {
const pairCounts = {};
const prevCounts = {};
for (let i = 1; i < text.length; i++) {
const prev = text[i - 1], ch = text[i];
prevCounts[prev] = (prevCounts[prev] || 0) + 1;
pairCounts[prev] = pairCounts[prev] || {};
pairCounts[prev][ch] = (pairCounts[prev][ch] || 0) + 1;
}
const total = text.length - 1;
let H = 0;
for (const prev in pairCounts) {
const totalPrev = prevCounts[prev];
for (const ch in pairCounts[prev]) {
const jointP = pairCounts[prev][ch] / total;
const condP = pairCounts[prev][ch] / totalPrev;
H -= jointP * Math.log2(condP);
}
}
return H;
}
const N = TEXT.length;
const RAW_BPC = 8;
const FLOOR_BPC = order1BitsPerChar(TEXT); // the order-1 entropy is the lowest floor this demo computes; real compressors chase an even lower, uncomputable floor (Kolmogorov complexity)
function renderText(mode) {
const box = document.getElementById('textbox');
box.innerHTML = '';
const freq = {};
for (const ch of TEXT) freq[ch] = (freq[ch] || 0) + 1;
const maxFreq = Math.max(...Object.values(freq));
for (const ch of TEXT) {
const span = document.createElement('span');
span.className = 'ch';
span.textContent = ch === ' ' ? ' ' : ch;
if (mode === 'freq' || mode === 'ctx') {
const weight = freq[ch] / maxFreq;
const alpha = 0.15 + 0.55 * weight;
span.style.background = `rgba(109,40,217,${alpha.toFixed(2)})`;
span.style.color = weight > 0.55 ? '#fff' : '#222';
}
box.appendChild(span);
}
}
function setMeter(bpc, statusKey, statusClass) {
const pct = Math.max(4, Math.min(100, (bpc / RAW_BPC) * 100));
document.getElementById('bpc').textContent = bpc.toFixed(3);
document.getElementById('barFill').style.width = pct + '%';
document.getElementById('totalBits').textContent = Math.round(bpc * N);
document.getElementById('floorVal').textContent = FLOOR_BPC.toFixed(3) + ' ' + 'bits/char';
const s = document.getElementById('status');
s.textContent = statusKey;
s.className = 'status ' + (statusClass || '');
}
function useRaw() {
renderText('raw');
setMeter(RAW_BPC, 'Every character costs a fixed 8 bits, no matter how common it is.', '');
}
function useFreq() {
renderText('freq');
const bpc = order0BitsPerChar(TEXT);
setMeter(bpc, i18nFmt('Common letters cost fewer bits than rare ones — %pct%% smaller than raw.', { pct: pctDrop(RAW_BPC, bpc) }), '');
}
function useCtx() {
renderText('ctx');
const bpc = order1BitsPerChar(TEXT);
setMeter(bpc, i18nFmt('Each character is predicted from the one before it — %pct%% smaller than raw, and closest yet to the entropy floor.', { pct: pctDrop(RAW_BPC, bpc) }), 'ok');
}
function pctDrop(from, to) {
return Math.round((1 - to / from) * 100);
}
document.getElementById('raw').addEventListener('click', useRaw);
document.getElementById('freq').addEventListener('click', useFreq);
document.getElementById('ctx').addEventListener('click', useCtx);
document.getElementById('reset').addEventListener('click', useRaw);
useRaw();
<p class="hint">Same short text, three ways to store it. Click each button and watch the bits-per-character needed for a lossless copy fall toward the text's own entropy floor.</p>
<div class="textbox" id="textbox"></div>
<div class="btns">
<button id="raw" type="button">Raw (8 bits/char)</button>
<button id="freq" type="button">Frequency-based</button>
<button id="ctx" type="button">Order-1 model</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="meter">
<div class="meter-row">
<span class="meter-label">Bits per character:</span>
<span class="meter-value" id="bpc">8.000</span>
</div>
<div class="bar-track"><div class="bar-fill" id="barFill"></div></div>
<div class="meter-row small">
<span>Total: <b id="totalBits">448</b> bits</span>
<span>Entropy floor: <b id="floorVal">–</b></span>
</div>
</div>
<div class="status" id="status">Pick a strategy to compress the text above.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.textbox { font: 600 15px ui-monospace, monospace; background: #f3f4f6; border: 1px solid #d9dce1;
border-radius: 8px; padding: .6rem .7rem; line-height: 1.6; word-break: break-word;
margin-bottom: .6rem; }
.textbox .ch { padding: 0 1px; border-radius: 2px; transition: background .25s, color .25s; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #6d28d9;
background: #6d28d9; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #6d28d9; }
.meter { background: #fafafa; border: 1px solid #e5e7eb; border-radius: 8px; padding: .6rem .7rem; }
.meter-row { display: flex; justify-content: space-between; font-size: .9rem; margin-bottom: .3rem; }
.meter-row.small { font-size: .78rem; color: #555; margin-top: .35rem; margin-bottom: 0; }
.meter-label { font-weight: 600; }
.meter-value { font: 700 1.1rem ui-monospace, monospace; color: #6d28d9; }
.bar-track { height: 14px; background: #e5e7eb; border-radius: 7px; overflow: hidden; }
.bar-fill { height: 100%; width: 100%; background: linear-gradient(90deg,#6d28d9,#a855f7);
border-radius: 7px; transition: width .5s ease; }
.status { font-size: .9rem; font-weight: 600; margin: .6rem 0 0; min-height: 1.3em; color: #444; }
.status.ok { color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A short, repetitive demo text — real Hutter Prize entries work on enwik9, a 10^9-byte slice of Wikipedia, but the same three ideas apply.
const TEXT = "the cat sat on the mat the cat ran and the cat sat again";
// Order-0 (frequency) entropy: treats each character independently, weighted by how often it appears — the theoretical limit for any code that ignores context (Shannon, Huffman).
function order0BitsPerChar(text) {
const freq = {};
for (const ch of text) freq[ch] = (freq[ch] || 0) + 1;
const n = text.length;
let H = 0;
for (const ch in freq) {
const p = freq[ch] / n;
H -= p * Math.log2(p);
}
return H;
}
// Order-1 (context) entropy: predicts each character from the single character before it, capturing letter-pair patterns like "th" and "at" — a tiny cousin of the context models real compressors use.
function order1BitsPerChar(text) {
const pairCounts = {};
const prevCounts = {};
for (let i = 1; i < text.length; i++) {
const prev = text[i - 1], ch = text[i];
prevCounts[prev] = (prevCounts[prev] || 0) + 1;
pairCounts[prev] = pairCounts[prev] || {};
pairCounts[prev][ch] = (pairCounts[prev][ch] || 0) + 1;
}
const total = text.length - 1;
let H = 0;
for (const prev in pairCounts) {
const totalPrev = prevCounts[prev];
for (const ch in pairCounts[prev]) {
const jointP = pairCounts[prev][ch] / total;
const condP = pairCounts[prev][ch] / totalPrev;
H -= jointP * Math.log2(condP);
}
}
return H;
}
const N = TEXT.length;
const RAW_BPC = 8;
const FLOOR_BPC = order1BitsPerChar(TEXT); // the order-1 entropy is the lowest floor this demo computes; real compressors chase an even lower, uncomputable floor (Kolmogorov complexity)
function renderText(mode) {
const box = document.getElementById('textbox');
box.innerHTML = '';
const freq = {};
for (const ch of TEXT) freq[ch] = (freq[ch] || 0) + 1;
const maxFreq = Math.max(...Object.values(freq));
for (const ch of TEXT) {
const span = document.createElement('span');
span.className = 'ch';
span.textContent = ch === ' ' ? ' ' : ch;
if (mode === 'freq' || mode === 'ctx') {
const weight = freq[ch] / maxFreq;
const alpha = 0.15 + 0.55 * weight;
span.style.background = `rgba(109,40,217,${alpha.toFixed(2)})`;
span.style.color = weight > 0.55 ? '#fff' : '#222';
}
box.appendChild(span);
}
}
function setMeter(bpc, statusKey, statusClass) {
const pct = Math.max(4, Math.min(100, (bpc / RAW_BPC) * 100));
document.getElementById('bpc').textContent = bpc.toFixed(3);
document.getElementById('barFill').style.width = pct + '%';
document.getElementById('totalBits').textContent = Math.round(bpc * N);
document.getElementById('floorVal').textContent = FLOOR_BPC.toFixed(3) + ' ' + 'bits/char';
const s = document.getElementById('status');
s.textContent = statusKey;
s.className = 'status ' + (statusClass || '');
}
function useRaw() {
renderText('raw');
setMeter(RAW_BPC, 'Every character costs a fixed 8 bits, no matter how common it is.', '');
}
function useFreq() {
renderText('freq');
const bpc = order0BitsPerChar(TEXT);
setMeter(bpc, i18nFmt('Common letters cost fewer bits than rare ones — %pct%% smaller than raw.', { pct: pctDrop(RAW_BPC, bpc) }), '');
}
function useCtx() {
renderText('ctx');
const bpc = order1BitsPerChar(TEXT);
setMeter(bpc, i18nFmt('Each character is predicted from the one before it — %pct%% smaller than raw, and closest yet to the entropy floor.', { pct: pctDrop(RAW_BPC, bpc) }), 'ok');
}
function pctDrop(from, to) {
return Math.round((1 - to / from) * 100);
}
document.getElementById('raw').addEventListener('click', useRaw);
document.getElementById('freq').addEventListener('click', useFreq);
document.getElementById('ctx').addEventListener('click', useCtx);
document.getElementById('reset').addEventListener('click', useRaw);
useRaw();
Raw spends a fixed 8 bits per character, no matter what. Frequency-based notices that some letters are far more common than others and spends fewer bits on them — that is the classic Huffman/Shannon move. Order-1 model goes further: it predicts each character from the one before it, the same trick modern language models use at enormous scale. Watch the bits-per-character drop toward the text's own entropy floor — the theoretical limit below which no lossless compressor can go without knowing more about the source.
Comments
Loading comments...