Below is a smooth signal that we have to store. The only knob is the bit budget: how many levels we are allowed to round each sample to. Drag the slider — fewer bits means coarser rounding and a blockier reconstruction; more bits means a faithful copy at a larger size.
<p class="hint">A smooth signal must be stored by rounding each sample to one of a fixed number of levels. <b>Fewer levels = fewer bits = coarser reconstruction.</b> Drag the slider to spend more or fewer bits and watch the trade-off.</p>
<div class="dials">
<label>Bit budget:
<input id="bits" type="range" min="1" max="6" step="1" value="3">
<span id="bitsVal">3 bits / sample</span>
</label>
</div>
<canvas id="plot" width="460" height="200"></canvas>
<div class="readout">
<div class="stat"><span class="k">Rate</span><span class="v" id="rate">—</span><span class="u">bits / sample</span></div>
<div class="stat"><span class="k">Levels</span><span class="v" id="levels">—</span><span class="u">round-to values</span></div>
<div class="stat"><span class="k">Distortion</span><span class="v" id="dist">—</span><span class="u">mean squared error</span></div>
</div>
<p class="note" id="note"></p>
* { 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; }
.dials { margin: .3rem 0 .6rem; }
label { font: 600 14px system-ui, sans-serif; display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
input[type=range] { flex: 1 1 180px; accent-color: #1d3557; }
#bitsVal { font-variant-numeric: tabular-nums; color: #1d3557; min-width: 9ch; }
canvas { width: 100%; max-width: 460px; height: auto; border: 1px solid #cdd9e3;
border-radius: 8px; background: #f7fafc; display: block; }
.readout { display: flex; gap: .5rem; flex-wrap: wrap; margin: .7rem 0 .2rem; }
.stat { flex: 1 1 120px; background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px;
padding: .5rem .6rem; display: flex; flex-direction: column; }
.stat .k { font: 600 12px system-ui; color: #5a7088; text-transform: uppercase; letter-spacing: .03em; }
.stat .v { font: 800 22px ui-monospace, monospace; color: #1d3557; font-variant-numeric: tabular-nums; }
.stat .u { font: 500 11px system-ui; color: #6b7785; }
.note { font-size: .85rem; color: #555; min-height: 2.4em; margin: .4rem 0 0; line-height: 1.4; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Rate-distortion in miniature. A smooth source signal is uniformly quantized to L = 2^bits levels. RATE = bits per sample. DISTORTION = mean squared error between the original samples and their rounded reconstructions. As the bit budget rises, distortion falls — tracing a rate-distortion curve. Shannon's R(D) (1959) is the lowest such curve any coder could ever achieve.
const N = 64;
// Fixed smooth source in [0,1]: sum of a couple of sines, normalised.
const src = [];
for (let i = 0; i < N; i++) {
const t = i / N;
let y = 0.5 + 0.32 * Math.sin(2 * Math.PI * t) + 0.14 * Math.sin(6 * Math.PI * t + 1);
src.push(Math.min(1, Math.max(0, y)));
}
// Uniform quantizer with L levels over [0,1]; returns reconstructed value.
function quantize(x, L) {
if (L <= 1) return 0.5; // 1 level: everything to the midpoint
const step = 1 / (L - 1);
const idx = Math.round(x / step); // nearest level index
return idx * step; // reconstructed value
}
function distortion(L) {
let s = 0;
for (const x of src) { const d = x - quantize(x, L); s += d * d; }
return s / N;
}
const cv = document.getElementById('plot');
const ctx = cv.getContext('2d');
function draw(L) {
const W = cv.width, H = cv.height, pad = 8;
ctx.clearRect(0, 0, W, H);
const X = i => pad + (i / (N - 1)) * (W - 2 * pad);
const Y = v => H - pad - v * (H - 2 * pad);
// reconstruction (stepped) in brand colour
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2; ctx.beginPath();
for (let i = 0; i < N; i++) {
const r = quantize(src[i], L);
if (i === 0) ctx.moveTo(X(i), Y(r)); else ctx.lineTo(X(i), Y(r));
}
ctx.stroke();
// original signal on top
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2; ctx.beginPath();
for (let i = 0; i < N; i++) {
if (i === 0) ctx.moveTo(X(i), Y(src[i])); else ctx.lineTo(X(i), Y(src[i]));
}
ctx.stroke();
}
function update() {
const bits = +document.getElementById('bits').value;
const L = Math.pow(2, bits);
const D = distortion(L);
document.getElementById('bitsVal').textContent = fmt('%bits% bit%v% / sample', { bits: bits, v: bits > 1 ? 's' : '' });
document.getElementById('rate').textContent = bits;
document.getElementById('levels').textContent = L;
document.getElementById('dist').textContent = D.toFixed(4);
const note = document.getElementById('note');
if (bits <= 1) note.textContent = 'Almost no bits: the signal collapses toward a flat line. Maximum distortion, minimum size.';
else if (bits >= 6) note.textContent = 'Plenty of bits: the red reconstruction hugs the blue original. Near-zero distortion, largest size.';
else note.textContent = 'Spend one more bit and you roughly halve the squared error — you are sliding down the rate-distortion curve.';
draw(L);
console.log(fmt('bits=%bits%', { bits: bits }), fmt('levels=%L%', { L: L }), fmt('MSE=%D%', { D: D.toFixed(5) }));
}
document.getElementById('bits').addEventListener('input', update);
update();
<p class="hint">A smooth signal must be stored by rounding each sample to one of a fixed number of levels. <b>Fewer levels = fewer bits = coarser reconstruction.</b> Drag the slider to spend more or fewer bits and watch the trade-off.</p>
<div class="dials">
<label>Bit budget:
<input id="bits" type="range" min="1" max="6" step="1" value="3">
<span id="bitsVal">3 bits / sample</span>
</label>
</div>
<canvas id="plot" width="460" height="200"></canvas>
<div class="readout">
<div class="stat"><span class="k">Rate</span><span class="v" id="rate">—</span><span class="u">bits / sample</span></div>
<div class="stat"><span class="k">Levels</span><span class="v" id="levels">—</span><span class="u">round-to values</span></div>
<div class="stat"><span class="k">Distortion</span><span class="v" id="dist">—</span><span class="u">mean squared error</span></div>
</div>
<p class="note" id="note"></p>
* { 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; }
.dials { margin: .3rem 0 .6rem; }
label { font: 600 14px system-ui, sans-serif; display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
input[type=range] { flex: 1 1 180px; accent-color: #1d3557; }
#bitsVal { font-variant-numeric: tabular-nums; color: #1d3557; min-width: 9ch; }
canvas { width: 100%; max-width: 460px; height: auto; border: 1px solid #cdd9e3;
border-radius: 8px; background: #f7fafc; display: block; }
.readout { display: flex; gap: .5rem; flex-wrap: wrap; margin: .7rem 0 .2rem; }
.stat { flex: 1 1 120px; background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px;
padding: .5rem .6rem; display: flex; flex-direction: column; }
.stat .k { font: 600 12px system-ui; color: #5a7088; text-transform: uppercase; letter-spacing: .03em; }
.stat .v { font: 800 22px ui-monospace, monospace; color: #1d3557; font-variant-numeric: tabular-nums; }
.stat .u { font: 500 11px system-ui; color: #6b7785; }
.note { font-size: .85rem; color: #555; min-height: 2.4em; margin: .4rem 0 0; line-height: 1.4; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Rate-distortion in miniature. A smooth source signal is uniformly quantized to L = 2^bits levels. RATE = bits per sample. DISTORTION = mean squared error between the original samples and their rounded reconstructions. As the bit budget rises, distortion falls — tracing a rate-distortion curve. Shannon's R(D) (1959) is the lowest such curve any coder could ever achieve.
const N = 64;
// Fixed smooth source in [0,1]: sum of a couple of sines, normalised.
const src = [];
for (let i = 0; i < N; i++) {
const t = i / N;
let y = 0.5 + 0.32 * Math.sin(2 * Math.PI * t) + 0.14 * Math.sin(6 * Math.PI * t + 1);
src.push(Math.min(1, Math.max(0, y)));
}
// Uniform quantizer with L levels over [0,1]; returns reconstructed value.
function quantize(x, L) {
if (L <= 1) return 0.5; // 1 level: everything to the midpoint
const step = 1 / (L - 1);
const idx = Math.round(x / step); // nearest level index
return idx * step; // reconstructed value
}
function distortion(L) {
let s = 0;
for (const x of src) { const d = x - quantize(x, L); s += d * d; }
return s / N;
}
const cv = document.getElementById('plot');
const ctx = cv.getContext('2d');
function draw(L) {
const W = cv.width, H = cv.height, pad = 8;
ctx.clearRect(0, 0, W, H);
const X = i => pad + (i / (N - 1)) * (W - 2 * pad);
const Y = v => H - pad - v * (H - 2 * pad);
// reconstruction (stepped) in brand colour
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2; ctx.beginPath();
for (let i = 0; i < N; i++) {
const r = quantize(src[i], L);
if (i === 0) ctx.moveTo(X(i), Y(r)); else ctx.lineTo(X(i), Y(r));
}
ctx.stroke();
// original signal on top
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2; ctx.beginPath();
for (let i = 0; i < N; i++) {
if (i === 0) ctx.moveTo(X(i), Y(src[i])); else ctx.lineTo(X(i), Y(src[i]));
}
ctx.stroke();
}
function update() {
const bits = +document.getElementById('bits').value;
const L = Math.pow(2, bits);
const D = distortion(L);
document.getElementById('bitsVal').textContent = fmt('%bits% bit%v% / sample', { bits: bits, v: bits > 1 ? 's' : '' });
document.getElementById('rate').textContent = bits;
document.getElementById('levels').textContent = L;
document.getElementById('dist').textContent = D.toFixed(4);
const note = document.getElementById('note');
if (bits <= 1) note.textContent = 'Almost no bits: the signal collapses toward a flat line. Maximum distortion, minimum size.';
else if (bits >= 6) note.textContent = 'Plenty of bits: the red reconstruction hugs the blue original. Near-zero distortion, largest size.';
else note.textContent = 'Spend one more bit and you roughly halve the squared error — you are sliding down the rate-distortion curve.';
draw(L);
console.log(fmt('bits=%bits%', { bits: bits }), fmt('levels=%L%', { L: L }), fmt('MSE=%D%', { D: D.toFixed(5) }));
}
document.getElementById('bits').addEventListener('input', update);
update();
Watch the two numbers move in opposite directions. As the rate (bits per sample) climbs, the distortion (average squared error) falls — but never to zero until you pay the full price. The smooth curve they trace out is a rate-distortion curve: every point is the best fidelity that this many bits can buy. Real codecs chase this same trade-off; Shannon's R(D) marks the lowest curve any of them could ever reach.
Comments
Loading comments...