<p class="hint">Build a signal by clicking preset shapes, then adjust the <b>threshold</b> to discard small wavelet coefficients. The red curve is the compressed reconstruction. The bar chart shows wavelet coefficients โ zeroed ones are grayed out.</p>
<div class="controls-row">
<div class="ctrl-group">
<label>Signal preset</label>
<div id="presets" class="btn-group">
<button class="preset active" data-id="smooth">Smooth</button>
<button class="preset" data-id="step">Step</button>
<button class="preset" data-id="spiky">Spiky</button>
<button class="preset" data-id="mixed">Mixed</button>
</div>
</div>
<div class="ctrl-group">
<label>Threshold <span id="tval">0.00</span></label>
<input type="range" id="thresh" min="0" max="100" value="0" step="1">
</div>
</div>
<canvas id="cv" width="560" height="180"></canvas>
<div id="coeff-wrap">
<canvas id="cc" width="560" height="90"></canvas>
</div>
<div id="stats">Kept <span id="kept">32</span> / 32 / 32 coefficients | Compression ratio <span id="ratio">1.0x</span></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls-row { display: flex; gap: 1.2rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: .6rem; }
.ctrl-group { display: flex; flex-direction: column; gap: .25rem; }
.ctrl-group label { font-size: .8rem; font-weight: 600; color: #555; }
.btn-group { display: flex; gap: .3rem; flex-wrap: wrap; }
.preset { font: 600 13px system-ui; padding: .3rem .65rem; border-radius: 6px;
border: 1px solid #1d3557; background: #fff; color: #1d3557; cursor: pointer; }
.preset.active { background: #1d3557; color: #fff; }
input[type=range] { width: 180px; accent-color: #1d3557; cursor: pointer; }
canvas { display: block; border: 1px solid #dde3eb; border-radius: 8px; background: #f8fafc;
max-width: 100%; }
#coeff-wrap { margin-top: .5rem; }
#stats { font-size: .85rem; color: #444; margin-top: .4rem; }
#stats span { font-weight: 700; color: #1d3557; }
// Haar wavelet transform and compression demo.
// The forward transform (standard non-interleaved form) repeatedly pairs adjacent
// values into averages (coarse) and differences (detail), storing them in separate
// halves so successive passes only touch the averages. The inverse undoes each pass.
const N = 32; // must be a power of 2
// โโ Signal presets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const PRESETS = {
smooth: () => Array.from({length:N}, (_,i) =>
Math.sin(2*Math.PI*i/N) + 0.4*Math.sin(6*Math.PI*i/N)),
step: () => Array.from({length:N}, (_,i) => i < N/2 ? 0.8 : -0.8),
spiky: () => { const a = Array(N).fill(0);
a[4]=1.8; a[5]=1.6; a[15]=-1.5; a[24]=1.2; a[25]=0.9; return a; },
mixed: () => Array.from({length:N}, (_,i) =>
0.6*Math.sin(2*Math.PI*i/N) + (i===10?1.4:0) + (i===22?-1:0) + (i<8?0.4:0))
};
const SQRT2 = Math.SQRT2;
// โโ Haar forward transform โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Layout of result: [finalApprox, coarsestDetail, ..., finestDetail_0, finestDetail_1]
// i.e. averages occupy [0..half-1] at each level, details occupy [half..len-1].
function haarFwd(sig) {
const c = sig.slice();
let len = c.length;
while (len > 1) {
const half = len >> 1;
const avgs = new Array(half);
const dets = new Array(half);
for (let i = 0; i < half; i++) {
avgs[i] = (c[2*i] + c[2*i+1]) / SQRT2;
dets[i] = (c[2*i] - c[2*i+1]) / SQRT2;
}
for (let i = 0; i < half; i++) { c[i] = avgs[i]; c[half+i] = dets[i]; }
len >>= 1;
}
return c;
}
// โโ Haar inverse transform โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function haarInv(c) {
const sig = c.slice();
const n = sig.length;
let len = 1;
while (len < n) {
len <<= 1;
const half = len >> 1;
const tmp = new Array(len);
for (let i = 0; i < half; i++) {
const avg = sig[i];
const det = sig[half + i];
tmp[2*i] = (avg + det) / SQRT2;
tmp[2*i+1] = (avg - det) / SQRT2;
}
for (let i = 0; i < len; i++) sig[i] = tmp[i];
}
return sig;
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let signal = PRESETS.smooth();
let thresh = 0;
// โโ Canvas refs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cvMain = document.getElementById('cv');
const ctxM = cvMain.getContext('2d');
const cvCoef = document.getElementById('cc');
const ctxC = cvCoef.getContext('2d');
// โโ Draw signal and reconstruction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawSignals(orig, recon) {
const W = cvMain.width, H = cvMain.height;
ctxM.clearRect(0, 0, W, H);
const pad = 20;
const plotW = W - 2*pad, plotH = H - 2*pad;
const all = [...orig, ...recon];
const mn = Math.min(...all), mx = Math.max(...all);
const range = mx - mn || 1;
function toX(i) { return pad + (i / (N-1)) * plotW; }
function toY(v) { return pad + (1 - (v - mn) / range) * plotH; }
// zero baseline
ctxM.strokeStyle = '#dde3eb'; ctxM.lineWidth = 1;
ctxM.beginPath();
const y0 = toY(0);
ctxM.moveTo(pad, y0); ctxM.lineTo(pad + plotW, y0); ctxM.stroke();
// original (blue, solid)
ctxM.strokeStyle = '#1d3557'; ctxM.lineWidth = 2; ctxM.setLineDash([]);
ctxM.beginPath();
orig.forEach((v,i) => { const x=toX(i),y=toY(v); i===0?ctxM.moveTo(x,y):ctxM.lineTo(x,y); });
ctxM.stroke();
// reconstruction (red, dashed)
ctxM.strokeStyle = '#e63946'; ctxM.lineWidth = 2; ctxM.setLineDash([4,3]);
ctxM.beginPath();
recon.forEach((v,i) => { const x=toX(i),y=toY(v); i===0?ctxM.moveTo(x,y):ctxM.lineTo(x,y); });
ctxM.stroke();
ctxM.setLineDash([]);
// legend
ctxM.font = '11px system-ui'; ctxM.textBaseline = 'top';
ctxM.fillStyle = '#1d3557';
ctxM.fillRect(pad, pad+2, 14, 3);
ctxM.fillText('Original', pad+18, pad-1);
ctxM.fillStyle = '#e63946';
ctxM.fillRect(pad+90, pad+2, 14, 3);
ctxM.fillText('Reconstructed', pad+108, pad-1);
}
// โโ Draw coefficient bar chart โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawCoefficients(coeffs, threshVal) {
const W = cvCoef.width, H = cvCoef.height;
ctxC.clearRect(0, 0, W, H);
const pad = 18;
const barW = (W - 2*pad) / N;
const maxAbs = Math.max(...coeffs.map(Math.abs), 0.001);
const midY = pad + (H - 2*pad) * 0.5;
const scaleY = (H - 2*pad) * 0.46;
// threshold lines (orange dashed)
if (threshVal > 0) {
ctxC.strokeStyle = '#f4a261'; ctxC.lineWidth = 1; ctxC.setLineDash([3,3]);
[midY - threshVal/maxAbs*scaleY, midY + threshVal/maxAbs*scaleY].forEach(ly => {
ctxC.beginPath(); ctxC.moveTo(pad, ly); ctxC.lineTo(W-pad, ly); ctxC.stroke();
});
ctxC.setLineDash([]);
}
coeffs.forEach((v, i) => {
// index 0 is the final approximation (DC), always keep it
const kept = (i === 0) || Math.abs(v) >= threshVal;
const x = pad + i * barW;
const bh = Math.abs(v) / maxAbs * scaleY;
ctxC.fillStyle = kept ? '#1d3557' : '#c8cdd4';
ctxC.fillRect(x + 1, v >= 0 ? midY - bh : midY, barW - 2, bh || 1);
});
// label
ctxC.fillStyle = '#888'; ctxC.font = '10px system-ui'; ctxC.textBaseline = 'top';
ctxC.fillText('Wavelet coefficients (orange lines = threshold)', pad, 1);
}
// โโ Main update โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function update() {
const coeffs = haarFwd(signal);
const maxDetail = Math.max(...coeffs.slice(1).map(Math.abs), 0.001);
const threshVal = (thresh / 100) * maxDetail;
// apply threshold (never zero out index 0 = DC approximation)
const compressed = coeffs.map((v, i) => (i===0 || Math.abs(v)>=threshVal) ? v : 0);
const kept = compressed.filter((v, i) => v !== 0 || i === 0).length;
const recon = haarInv(compressed);
document.getElementById('kept').textContent = kept;
const ratio = kept > 0 ? (N / kept).toFixed(1) : 'inf';
document.getElementById('ratio').textContent = ratio + 'x';
drawSignals(signal, recon);
drawCoefficients(coeffs, threshVal);
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.querySelectorAll('.preset').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
signal = PRESETS[btn.dataset.id]();
document.getElementById('thresh').value = 0;
thresh = 0;
document.getElementById('tval').textContent = '0.00';
update();
});
});
document.getElementById('thresh').addEventListener('input', e => {
thresh = +e.target.value;
const coeffs = haarFwd(signal);
const maxDetail = Math.max(...coeffs.slice(1).map(Math.abs), 0.001);
const tv = (thresh / 100) * maxDetail;
document.getElementById('tval').textContent = tv.toFixed(2);
update();
});
update();
<p class="hint">Build a signal by clicking preset shapes, then adjust the <b>threshold</b> to discard small wavelet coefficients. The red curve is the compressed reconstruction. The bar chart shows wavelet coefficients โ zeroed ones are grayed out.</p>
<div class="controls-row">
<div class="ctrl-group">
<label>Signal preset</label>
<div id="presets" class="btn-group">
<button class="preset active" data-id="smooth">Smooth</button>
<button class="preset" data-id="step">Step</button>
<button class="preset" data-id="spiky">Spiky</button>
<button class="preset" data-id="mixed">Mixed</button>
</div>
</div>
<div class="ctrl-group">
<label>Threshold <span id="tval">0.00</span></label>
<input type="range" id="thresh" min="0" max="100" value="0" step="1">
</div>
</div>
<canvas id="cv" width="560" height="180"></canvas>
<div id="coeff-wrap">
<canvas id="cc" width="560" height="90"></canvas>
</div>
<div id="stats">Kept <span id="kept">32</span> / 32 / 32 coefficients | Compression ratio <span id="ratio">1.0x</span></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls-row { display: flex; gap: 1.2rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: .6rem; }
.ctrl-group { display: flex; flex-direction: column; gap: .25rem; }
.ctrl-group label { font-size: .8rem; font-weight: 600; color: #555; }
.btn-group { display: flex; gap: .3rem; flex-wrap: wrap; }
.preset { font: 600 13px system-ui; padding: .3rem .65rem; border-radius: 6px;
border: 1px solid #1d3557; background: #fff; color: #1d3557; cursor: pointer; }
.preset.active { background: #1d3557; color: #fff; }
input[type=range] { width: 180px; accent-color: #1d3557; cursor: pointer; }
canvas { display: block; border: 1px solid #dde3eb; border-radius: 8px; background: #f8fafc;
max-width: 100%; }
#coeff-wrap { margin-top: .5rem; }
#stats { font-size: .85rem; color: #444; margin-top: .4rem; }
#stats span { font-weight: 700; color: #1d3557; }
// Haar wavelet transform and compression demo.
// The forward transform (standard non-interleaved form) repeatedly pairs adjacent
// values into averages (coarse) and differences (detail), storing them in separate
// halves so successive passes only touch the averages. The inverse undoes each pass.
const N = 32; // must be a power of 2
// โโ Signal presets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const PRESETS = {
smooth: () => Array.from({length:N}, (_,i) =>
Math.sin(2*Math.PI*i/N) + 0.4*Math.sin(6*Math.PI*i/N)),
step: () => Array.from({length:N}, (_,i) => i < N/2 ? 0.8 : -0.8),
spiky: () => { const a = Array(N).fill(0);
a[4]=1.8; a[5]=1.6; a[15]=-1.5; a[24]=1.2; a[25]=0.9; return a; },
mixed: () => Array.from({length:N}, (_,i) =>
0.6*Math.sin(2*Math.PI*i/N) + (i===10?1.4:0) + (i===22?-1:0) + (i<8?0.4:0))
};
const SQRT2 = Math.SQRT2;
// โโ Haar forward transform โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Layout of result: [finalApprox, coarsestDetail, ..., finestDetail_0, finestDetail_1]
// i.e. averages occupy [0..half-1] at each level, details occupy [half..len-1].
function haarFwd(sig) {
const c = sig.slice();
let len = c.length;
while (len > 1) {
const half = len >> 1;
const avgs = new Array(half);
const dets = new Array(half);
for (let i = 0; i < half; i++) {
avgs[i] = (c[2*i] + c[2*i+1]) / SQRT2;
dets[i] = (c[2*i] - c[2*i+1]) / SQRT2;
}
for (let i = 0; i < half; i++) { c[i] = avgs[i]; c[half+i] = dets[i]; }
len >>= 1;
}
return c;
}
// โโ Haar inverse transform โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function haarInv(c) {
const sig = c.slice();
const n = sig.length;
let len = 1;
while (len < n) {
len <<= 1;
const half = len >> 1;
const tmp = new Array(len);
for (let i = 0; i < half; i++) {
const avg = sig[i];
const det = sig[half + i];
tmp[2*i] = (avg + det) / SQRT2;
tmp[2*i+1] = (avg - det) / SQRT2;
}
for (let i = 0; i < len; i++) sig[i] = tmp[i];
}
return sig;
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let signal = PRESETS.smooth();
let thresh = 0;
// โโ Canvas refs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cvMain = document.getElementById('cv');
const ctxM = cvMain.getContext('2d');
const cvCoef = document.getElementById('cc');
const ctxC = cvCoef.getContext('2d');
// โโ Draw signal and reconstruction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawSignals(orig, recon) {
const W = cvMain.width, H = cvMain.height;
ctxM.clearRect(0, 0, W, H);
const pad = 20;
const plotW = W - 2*pad, plotH = H - 2*pad;
const all = [...orig, ...recon];
const mn = Math.min(...all), mx = Math.max(...all);
const range = mx - mn || 1;
function toX(i) { return pad + (i / (N-1)) * plotW; }
function toY(v) { return pad + (1 - (v - mn) / range) * plotH; }
// zero baseline
ctxM.strokeStyle = '#dde3eb'; ctxM.lineWidth = 1;
ctxM.beginPath();
const y0 = toY(0);
ctxM.moveTo(pad, y0); ctxM.lineTo(pad + plotW, y0); ctxM.stroke();
// original (blue, solid)
ctxM.strokeStyle = '#1d3557'; ctxM.lineWidth = 2; ctxM.setLineDash([]);
ctxM.beginPath();
orig.forEach((v,i) => { const x=toX(i),y=toY(v); i===0?ctxM.moveTo(x,y):ctxM.lineTo(x,y); });
ctxM.stroke();
// reconstruction (red, dashed)
ctxM.strokeStyle = '#e63946'; ctxM.lineWidth = 2; ctxM.setLineDash([4,3]);
ctxM.beginPath();
recon.forEach((v,i) => { const x=toX(i),y=toY(v); i===0?ctxM.moveTo(x,y):ctxM.lineTo(x,y); });
ctxM.stroke();
ctxM.setLineDash([]);
// legend
ctxM.font = '11px system-ui'; ctxM.textBaseline = 'top';
ctxM.fillStyle = '#1d3557';
ctxM.fillRect(pad, pad+2, 14, 3);
ctxM.fillText('Original', pad+18, pad-1);
ctxM.fillStyle = '#e63946';
ctxM.fillRect(pad+90, pad+2, 14, 3);
ctxM.fillText('Reconstructed', pad+108, pad-1);
}
// โโ Draw coefficient bar chart โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawCoefficients(coeffs, threshVal) {
const W = cvCoef.width, H = cvCoef.height;
ctxC.clearRect(0, 0, W, H);
const pad = 18;
const barW = (W - 2*pad) / N;
const maxAbs = Math.max(...coeffs.map(Math.abs), 0.001);
const midY = pad + (H - 2*pad) * 0.5;
const scaleY = (H - 2*pad) * 0.46;
// threshold lines (orange dashed)
if (threshVal > 0) {
ctxC.strokeStyle = '#f4a261'; ctxC.lineWidth = 1; ctxC.setLineDash([3,3]);
[midY - threshVal/maxAbs*scaleY, midY + threshVal/maxAbs*scaleY].forEach(ly => {
ctxC.beginPath(); ctxC.moveTo(pad, ly); ctxC.lineTo(W-pad, ly); ctxC.stroke();
});
ctxC.setLineDash([]);
}
coeffs.forEach((v, i) => {
// index 0 is the final approximation (DC), always keep it
const kept = (i === 0) || Math.abs(v) >= threshVal;
const x = pad + i * barW;
const bh = Math.abs(v) / maxAbs * scaleY;
ctxC.fillStyle = kept ? '#1d3557' : '#c8cdd4';
ctxC.fillRect(x + 1, v >= 0 ? midY - bh : midY, barW - 2, bh || 1);
});
// label
ctxC.fillStyle = '#888'; ctxC.font = '10px system-ui'; ctxC.textBaseline = 'top';
ctxC.fillText('Wavelet coefficients (orange lines = threshold)', pad, 1);
}
// โโ Main update โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function update() {
const coeffs = haarFwd(signal);
const maxDetail = Math.max(...coeffs.slice(1).map(Math.abs), 0.001);
const threshVal = (thresh / 100) * maxDetail;
// apply threshold (never zero out index 0 = DC approximation)
const compressed = coeffs.map((v, i) => (i===0 || Math.abs(v)>=threshVal) ? v : 0);
const kept = compressed.filter((v, i) => v !== 0 || i === 0).length;
const recon = haarInv(compressed);
document.getElementById('kept').textContent = kept;
const ratio = kept > 0 ? (N / kept).toFixed(1) : 'inf';
document.getElementById('ratio').textContent = ratio + 'x';
drawSignals(signal, recon);
drawCoefficients(coeffs, threshVal);
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.querySelectorAll('.preset').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.preset').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
signal = PRESETS[btn.dataset.id]();
document.getElementById('thresh').value = 0;
thresh = 0;
document.getElementById('tval').textContent = '0.00';
update();
});
});
document.getElementById('thresh').addEventListener('input', e => {
thresh = +e.target.value;
const coeffs = haarFwd(signal);
const maxDetail = Math.max(...coeffs.slice(1).map(Math.abs), 0.001);
const tv = (thresh / 100) * maxDetail;
document.getElementById('tval').textContent = tv.toFixed(2);
update();
});
update();
Comments
Loading comments...