The slider below controls how many DCT coefficients are kept from each 8ร8 block โ low numbers keep only the lowest-frequency information, high numbers keep almost everything. Watch how image quality and the retained data change.
<div class="controls">
<label for="quality">Kept coefficients: <strong id="qualLabel">10</strong> / 64</label>
<input type="range" id="quality" min="1" max="64" value="10">
<span class="size-note" id="sizeNote"></span>
</div>
<div class="canvases">
<div class="canvas-wrap">
<div class="caption">Original (8ร8 block)</div>
<canvas id="origCanvas" width="128" height="128"></canvas>
</div>
<div class="canvas-wrap">
<div class="caption">After DCT + keep <span id="keepLabel">10</span> coeffs</div>
<canvas id="dctCanvas" width="128" height="128"></canvas>
</div>
<div class="canvas-wrap">
<div class="caption">DCT coefficients (log scale)</div>
<canvas id="coeffCanvas" width="128" height="128"></canvas>
</div>
</div>
<div class="info" id="info"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .9rem; color: #333; }
input[type=range] { width: 100%; max-width: 340px; accent-color: #1d3557; }
.size-note { font-size: .8rem; color: #556; }
.canvases { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: .6rem; }
.canvas-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.caption { font-size: .78rem; color: #445; text-align: center; max-width: 128px; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; }
.info { font-size: .85rem; color: #1d3557; min-height: 1.4em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// JPEG-DCT demo: apply a real 2D DCT to an 8x8 synthetic "image" block,
// zero out the high-frequency coefficients (zig-zag order), reconstruct,
// and display original vs reconstructed vs coefficient heatmap.
const N = 8;
// โโ Synthetic 8ร8 luminance block (0-255) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// A plausible natural-image block: gentle gradient + a bright spot.
const ORIG = [
[120,122,124,126,128,130,132,134],
[118,130,135,128,126,128,130,132],
[116,128,150,145,130,126,128,130],
[114,126,145,160,150,135,126,128],
[112,124,130,150,155,148,130,126],
[110,122,126,135,148,145,128,124],
[108,120,124,126,130,128,126,122],
[106,118,120,122,124,122,120,118]
];
// โโ 1D DCT-II (orthonormal) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function dct1d(x) {
const n = x.length;
const out = new Array(n);
for (let k = 0; k < n; k++) {
let sum = 0;
for (let i = 0; i < n; i++) sum += x[i] * Math.cos(Math.PI * k * (2*i+1) / (2*n));
out[k] = sum * (k === 0 ? Math.sqrt(1/n) : Math.sqrt(2/n));
}
return out;
}
// โโ 2D DCT via separable 1D passes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function dct2d(block) {
const n = block.length;
// rows
let tmp = block.map(row => dct1d(row));
// columns
const out = Array.from({length: n}, () => new Array(n));
for (let c = 0; c < n; c++) {
const col = tmp.map(row => row[c]);
const dc = dct1d(col);
for (let r = 0; r < n; r++) out[r][c] = dc[r];
}
return out;
}
// โโ Inverse 1D DCT-III โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function idct1d(X) {
const n = X.length;
const out = new Array(n);
for (let i = 0; i < n; i++) {
let sum = X[0] * Math.sqrt(1/n);
for (let k = 1; k < n; k++)
sum += X[k] * Math.sqrt(2/n) * Math.cos(Math.PI * k * (2*i+1) / (2*n));
out[i] = sum;
}
return out;
}
function idct2d(C) {
const n = C.length;
// columns first
let tmp = Array.from({length: n}, () => new Array(n));
for (let c = 0; c < n; c++) {
const col = C.map(row => row[c]);
const ic = idct1d(col);
for (let r = 0; r < n; r++) tmp[r][c] = ic[r];
}
// rows
return tmp.map(row => idct1d(row));
}
// โโ Zig-zag scan order for 8ร8 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function zigzag8() {
const order = [];
let r = 0, c = 0, going_up = true;
while (order.length < 64) {
order.push([r, c]);
if (going_up) {
if (c === N-1) { r++; going_up = false; }
else if (r === 0) { c++; going_up = false; }
else { r--; c++; }
} else {
if (r === N-1) { c++; going_up = true; }
else if (c === 0) { r++; going_up = true; }
else { r++; c--; }
}
}
return order;
}
const ZZ = zigzag8();
// โโ Rendering helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawBlock(canvas, block, clamp) {
const ctx = canvas.getContext('2d');
const pw = canvas.width / N;
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
let v = Math.round(block[r][c]);
if (clamp) v = Math.max(0, Math.min(255, v));
ctx.fillStyle = `rgb(${v},${v},${v})`;
ctx.fillRect(c * pw, r * pw, pw, pw);
}
}
function drawCoefficients(canvas, C) {
const ctx = canvas.getContext('2d');
const pw = canvas.width / N;
// find max abs for normalization
let maxAbs = 0;
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++)
maxAbs = Math.max(maxAbs, Math.abs(C[r][c]));
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
const v = C[r][c];
// log-scale magnitude, color: red=positive, blue=negative
const mag = Math.log1p(Math.abs(v)) / Math.log1p(maxAbs);
const intensity = Math.round(mag * 255);
if (v >= 0) ctx.fillStyle = `rgb(${intensity},${Math.round(intensity*0.4)},0)`;
else ctx.fillStyle = `rgb(0,${Math.round(intensity*0.4)},${intensity})`;
ctx.fillRect(c * pw, r * pw, pw, pw);
}
}
// โโ Main update โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const dctFull = dct2d(ORIG);
function update(kept) {
// Zero out everything beyond the first `kept` zig-zag positions
const C = Array.from({length: N}, () => new Array(N).fill(0));
for (let i = 0; i < kept; i++) {
const [r, c] = ZZ[i];
C[r][c] = dctFull[r][c];
}
// Reconstruct
const rec = idct2d(C);
// Draw
drawBlock(document.getElementById('origCanvas'), ORIG, false);
drawBlock(document.getElementById('dctCanvas'), rec, true);
drawCoefficients(document.getElementById('coeffCanvas'), C);
// Compute MSE
let mse = 0;
for (let r = 0; r < N; r++) for (let cc = 0; cc < N; cc++) {
const diff = ORIG[r][cc] - Math.max(0, Math.min(255, rec[r][cc]));
mse += diff * diff;
}
mse /= (N * N);
const psnr = mse < 0.0001 ? Infinity : 10 * Math.log10(255*255 / mse);
const pct = Math.round(kept / 64 * 100);
document.getElementById('info').textContent =
fmt('Keeping %pct%% % of coefficients. MSE = %mse%. PSNR = %v%.', { pct: pct, mse: mse.toFixed(2), v: isFinite(psnr) ? psnr.toFixed(1) + ' dB' : 'โ' });
document.getElementById('sizeNote').textContent =
fmt('(Discarding %v% of 64 coefficients โ %v2%% % of frequency data thrown away)', { v: 64 - kept, v2: 100 - pct });
console.assert(kept === 64 ? mse < 0.01 : true, 'Full reconstruction should be lossless');
}
const slider = document.getElementById('quality');
const qualLabel = document.getElementById('qualLabel');
const keepLabel = document.getElementById('keepLabel');
slider.addEventListener('input', () => {
const k = parseInt(slider.value);
qualLabel.textContent = k;
keepLabel.textContent = k;
update(k);
});
update(parseInt(slider.value));
// โโ Node.js verification (ignored in browser) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (typeof module !== 'undefined') module.exports = { dct2d, idct2d, ZZ, ORIG };
<div class="controls">
<label for="quality">Kept coefficients: <strong id="qualLabel">10</strong> / 64</label>
<input type="range" id="quality" min="1" max="64" value="10">
<span class="size-note" id="sizeNote"></span>
</div>
<div class="canvases">
<div class="canvas-wrap">
<div class="caption">Original (8ร8 block)</div>
<canvas id="origCanvas" width="128" height="128"></canvas>
</div>
<div class="canvas-wrap">
<div class="caption">After DCT + keep <span id="keepLabel">10</span> coeffs</div>
<canvas id="dctCanvas" width="128" height="128"></canvas>
</div>
<div class="canvas-wrap">
<div class="caption">DCT coefficients (log scale)</div>
<canvas id="coeffCanvas" width="128" height="128"></canvas>
</div>
</div>
<div class="info" id="info"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .9rem; color: #333; }
input[type=range] { width: 100%; max-width: 340px; accent-color: #1d3557; }
.size-note { font-size: .8rem; color: #556; }
.canvases { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: .6rem; }
.canvas-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.caption { font-size: .78rem; color: #445; text-align: center; max-width: 128px; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; }
.info { font-size: .85rem; color: #1d3557; min-height: 1.4em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// JPEG-DCT demo: apply a real 2D DCT to an 8x8 synthetic "image" block,
// zero out the high-frequency coefficients (zig-zag order), reconstruct,
// and display original vs reconstructed vs coefficient heatmap.
const N = 8;
// โโ Synthetic 8ร8 luminance block (0-255) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// A plausible natural-image block: gentle gradient + a bright spot.
const ORIG = [
[120,122,124,126,128,130,132,134],
[118,130,135,128,126,128,130,132],
[116,128,150,145,130,126,128,130],
[114,126,145,160,150,135,126,128],
[112,124,130,150,155,148,130,126],
[110,122,126,135,148,145,128,124],
[108,120,124,126,130,128,126,122],
[106,118,120,122,124,122,120,118]
];
// โโ 1D DCT-II (orthonormal) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function dct1d(x) {
const n = x.length;
const out = new Array(n);
for (let k = 0; k < n; k++) {
let sum = 0;
for (let i = 0; i < n; i++) sum += x[i] * Math.cos(Math.PI * k * (2*i+1) / (2*n));
out[k] = sum * (k === 0 ? Math.sqrt(1/n) : Math.sqrt(2/n));
}
return out;
}
// โโ 2D DCT via separable 1D passes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function dct2d(block) {
const n = block.length;
// rows
let tmp = block.map(row => dct1d(row));
// columns
const out = Array.from({length: n}, () => new Array(n));
for (let c = 0; c < n; c++) {
const col = tmp.map(row => row[c]);
const dc = dct1d(col);
for (let r = 0; r < n; r++) out[r][c] = dc[r];
}
return out;
}
// โโ Inverse 1D DCT-III โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function idct1d(X) {
const n = X.length;
const out = new Array(n);
for (let i = 0; i < n; i++) {
let sum = X[0] * Math.sqrt(1/n);
for (let k = 1; k < n; k++)
sum += X[k] * Math.sqrt(2/n) * Math.cos(Math.PI * k * (2*i+1) / (2*n));
out[i] = sum;
}
return out;
}
function idct2d(C) {
const n = C.length;
// columns first
let tmp = Array.from({length: n}, () => new Array(n));
for (let c = 0; c < n; c++) {
const col = C.map(row => row[c]);
const ic = idct1d(col);
for (let r = 0; r < n; r++) tmp[r][c] = ic[r];
}
// rows
return tmp.map(row => idct1d(row));
}
// โโ Zig-zag scan order for 8ร8 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function zigzag8() {
const order = [];
let r = 0, c = 0, going_up = true;
while (order.length < 64) {
order.push([r, c]);
if (going_up) {
if (c === N-1) { r++; going_up = false; }
else if (r === 0) { c++; going_up = false; }
else { r--; c++; }
} else {
if (r === N-1) { c++; going_up = true; }
else if (c === 0) { r++; going_up = true; }
else { r++; c--; }
}
}
return order;
}
const ZZ = zigzag8();
// โโ Rendering helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawBlock(canvas, block, clamp) {
const ctx = canvas.getContext('2d');
const pw = canvas.width / N;
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
let v = Math.round(block[r][c]);
if (clamp) v = Math.max(0, Math.min(255, v));
ctx.fillStyle = `rgb(${v},${v},${v})`;
ctx.fillRect(c * pw, r * pw, pw, pw);
}
}
function drawCoefficients(canvas, C) {
const ctx = canvas.getContext('2d');
const pw = canvas.width / N;
// find max abs for normalization
let maxAbs = 0;
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++)
maxAbs = Math.max(maxAbs, Math.abs(C[r][c]));
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
const v = C[r][c];
// log-scale magnitude, color: red=positive, blue=negative
const mag = Math.log1p(Math.abs(v)) / Math.log1p(maxAbs);
const intensity = Math.round(mag * 255);
if (v >= 0) ctx.fillStyle = `rgb(${intensity},${Math.round(intensity*0.4)},0)`;
else ctx.fillStyle = `rgb(0,${Math.round(intensity*0.4)},${intensity})`;
ctx.fillRect(c * pw, r * pw, pw, pw);
}
}
// โโ Main update โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const dctFull = dct2d(ORIG);
function update(kept) {
// Zero out everything beyond the first `kept` zig-zag positions
const C = Array.from({length: N}, () => new Array(N).fill(0));
for (let i = 0; i < kept; i++) {
const [r, c] = ZZ[i];
C[r][c] = dctFull[r][c];
}
// Reconstruct
const rec = idct2d(C);
// Draw
drawBlock(document.getElementById('origCanvas'), ORIG, false);
drawBlock(document.getElementById('dctCanvas'), rec, true);
drawCoefficients(document.getElementById('coeffCanvas'), C);
// Compute MSE
let mse = 0;
for (let r = 0; r < N; r++) for (let cc = 0; cc < N; cc++) {
const diff = ORIG[r][cc] - Math.max(0, Math.min(255, rec[r][cc]));
mse += diff * diff;
}
mse /= (N * N);
const psnr = mse < 0.0001 ? Infinity : 10 * Math.log10(255*255 / mse);
const pct = Math.round(kept / 64 * 100);
document.getElementById('info').textContent =
fmt('Keeping %pct%% % of coefficients. MSE = %mse%. PSNR = %v%.', { pct: pct, mse: mse.toFixed(2), v: isFinite(psnr) ? psnr.toFixed(1) + ' dB' : 'โ' });
document.getElementById('sizeNote').textContent =
fmt('(Discarding %v% of 64 coefficients โ %v2%% % of frequency data thrown away)', { v: 64 - kept, v2: 100 - pct });
console.assert(kept === 64 ? mse < 0.01 : true, 'Full reconstruction should be lossless');
}
const slider = document.getElementById('quality');
const qualLabel = document.getElementById('qualLabel');
const keepLabel = document.getElementById('keepLabel');
slider.addEventListener('input', () => {
const k = parseInt(slider.value);
qualLabel.textContent = k;
keepLabel.textContent = k;
update(k);
});
update(parseInt(slider.value));
// โโ Node.js verification (ignored in browser) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (typeof module !== 'undefined') module.exports = { dct2d, idct2d, ZZ, ORIG };
Notice that even at very low quality the broad shapes survive. The high-frequency coefficients โ which encode sharp edges and fine textures โ are the first to go. At mid quality the image already looks acceptable; the data savings are enormous. This is the essential trade-off JPEG makes: bet on what human vision cannot resolve, and be right almost every time.
Comments
Loading comments...