Click cells in the grid below to toggle them between white and black. RLE reads each row left to right and encodes every run of identical pixels as (color, count). Watch the encoded output update live as you draw.
<p class="hint">Click cells to toggle black/white. RLE reads each row left-to-right
and encodes every run of identical pixels as <b>(color, count)</b>.
Watch the output update live as you draw.</p>
<canvas id="grid" width="320" height="320"></canvas>
<div class="controls">
<button id="clear" type="button" class="ghost">Clear</button>
<button id="fill" type="button" class="ghost">Fill</button>
<button id="checker" type="button" class="ghost">Checkerboard</button>
</div>
<div class="stats" id="stats"></div>
<div class="output-wrap">
<div class="output-label">RLE output (row by row):</div>
<div id="output" class="output"></div>
</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; }
canvas { display: block; border: 1.5px solid #adb1b8; border-radius: 6px;
cursor: crosshair; touch-action: none; }
.controls { display: flex; gap: .5rem; margin: .6rem 0 .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border-radius: 7px;
border: 1.5px solid #1d3557; cursor: pointer; }
button:not(.ghost) { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .85rem; font-weight: 600; color: #555; margin-bottom: .5rem; min-height: 1.2em; }
.output-wrap { border: 1px solid #cdd9e3; border-radius: 6px; padding: .5rem .7rem;
background: #f7f9fb; max-height: 160px; overflow-y: auto; }
.output-label { font-size: .78rem; font-weight: 700; color: #888; margin-bottom: .3rem;
letter-spacing: .04em; text-transform: uppercase; }
.output { font: 13px/1.7 ui-monospace, monospace; color: #1d3557; word-break: break-all; }
.run-w { color: #888; }
.run-b { color: #1d3557; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Run-Length Encoding demo.
// Grid: COLS x ROWS cells. Each cell is 0 (white) or 1 (black).
// encode(row): produce (symbol,count) pairs scanning left to right.
// The demo also tracks raw pixel count vs encoded pair count to show the compression ratio live.
const COLS = 16, ROWS = 16;
const CELL = 20; // px per cell
const grid = new Array(ROWS).fill(null).map(() => new Array(COLS).fill(0));
const canvas = document.getElementById('grid');
const ctx = canvas.getContext('2d');
canvas.width = COLS * CELL;
canvas.height = ROWS * CELL;
// โโ RLE encode a single row โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function encodeRow(row) {
if (!row.length) return [];
const pairs = [];
let cur = row[0], cnt = 1;
for (let i = 1; i < row.length; i++) {
if (row[i] === cur) { cnt++; }
else { pairs.push([cur, cnt]); cur = row[i]; cnt = 1; }
}
pairs.push([cur, cnt]);
return pairs;
}
// โโ Encode the full grid and update UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateOutput() {
const outputEl = document.getElementById('output');
const statsEl = document.getElementById('stats');
let totalRaw = 0, totalPairs = 0;
const lines = [];
for (let r = 0; r < ROWS; r++) {
const pairs = encodeRow(grid[r]);
totalRaw += COLS;
totalPairs += pairs.length;
const spans = pairs.map(([sym, cnt]) => {
const cls = sym === 0 ? 'run-w' : 'run-b';
const label = sym === 0 ? 'W' : 'B';
return `<span class="${cls}">(${label},${cnt})</span>`;
});
lines.push(fmt('Row %r%%v%: %spans%', { r: r, v: 1, spans: spans.join(' ') }));
}
outputEl.innerHTML = lines.join('<br>');
const ratio = (totalPairs * 2 / totalRaw).toFixed(2);
const pct = Math.round((1 - totalPairs * 2 / totalRaw) * 100);
statsEl.textContent =
fmt('Raw: %totalRaw% pixels โ Encoded: %totalPairs% pairs (%v% values) | %v2%', { totalRaw: totalRaw, totalPairs: totalPairs, v: totalPairs * 2, v2: pct >= 0 ? 'Saved ' + pct + '%' : 'Expanded ' + (-pct) + '%' });
// Log for node verification
console.log('VERIFY encodeRow([1,1,0,0,0,1]):', JSON.stringify(encodeRow([1,1,0,0,0,1])));
console.log('VERIFY encodeRow([0,1,0,1,0,1]):', JSON.stringify(encodeRow([0,1,0,1,0,1])));
console.log('VERIFY encodeRow([0,0,0,0,0,0]):', JSON.stringify(encodeRow([0,0,0,0,0,0])));
}
// โโ Draw the canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
ctx.fillStyle = grid[r][c] === 1 ? '#1d3557' : '#f0f4f8';
ctx.fillRect(c * CELL, r * CELL, CELL, CELL);
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 0.5;
ctx.strokeRect(c * CELL + 0.25, r * CELL + 0.25, CELL - 0.5, CELL - 0.5);
}
}
updateOutput();
}
// โโ Mouse / touch input โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let painting = false, paintVal = 0;
function cellAt(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const c = Math.floor(x / CELL);
const r = Math.floor(y / CELL);
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) return [r, c];
return null;
}
canvas.addEventListener('mousedown', e => {
const pos = cellAt(e);
if (!pos) return;
painting = true;
paintVal = grid[pos[0]][pos[1]] === 0 ? 1 : 0;
grid[pos[0]][pos[1]] = paintVal;
draw();
});
canvas.addEventListener('mousemove', e => {
if (!painting) return;
const pos = cellAt(e);
if (pos) { grid[pos[0]][pos[1]] = paintVal; draw(); }
});
window.addEventListener('mouseup', () => { painting = false; });
canvas.addEventListener('touchstart', e => {
e.preventDefault();
const pos = cellAt(e.touches[0]);
if (!pos) return;
painting = true;
paintVal = grid[pos[0]][pos[1]] === 0 ? 1 : 0;
grid[pos[0]][pos[1]] = paintVal;
draw();
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
if (!painting) return;
const pos = cellAt(e.touches[0]);
if (pos) { grid[pos[0]][pos[1]] = paintVal; draw(); }
}, { passive: false });
window.addEventListener('touchend', () => { painting = false; });
// โโ Preset buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('clear').addEventListener('click', () => {
for (let r = 0; r < ROWS; r++) grid[r].fill(0);
draw();
});
document.getElementById('fill').addEventListener('click', () => {
for (let r = 0; r < ROWS; r++) grid[r].fill(1);
draw();
});
document.getElementById('checker').addEventListener('click', () => {
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
grid[r][c] = (r + c) % 2;
draw();
});
// โโ Initial pattern: a simple house silhouette โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// roof (rows 2-5 pyramid), walls (rows 6-13), door (rows 10-13 center)
(function initPattern() {
// Roof
[[2,7,8],[3,6,9],[4,5,10],[5,4,11]].forEach(([row,cL,cR]) => {
for (let c = cL; c <= cR; c++) grid[row][c] = 1;
});
// Walls
for (let r = 6; r <= 13; r++)
for (let c = 4; c <= 11; c++) grid[r][c] = 1;
// Door opening
for (let r = 10; r <= 13; r++)
for (let c = 7; c <= 8; c++) grid[r][c] = 0;
draw();
})();
<p class="hint">Click cells to toggle black/white. RLE reads each row left-to-right
and encodes every run of identical pixels as <b>(color, count)</b>.
Watch the output update live as you draw.</p>
<canvas id="grid" width="320" height="320"></canvas>
<div class="controls">
<button id="clear" type="button" class="ghost">Clear</button>
<button id="fill" type="button" class="ghost">Fill</button>
<button id="checker" type="button" class="ghost">Checkerboard</button>
</div>
<div class="stats" id="stats"></div>
<div class="output-wrap">
<div class="output-label">RLE output (row by row):</div>
<div id="output" class="output"></div>
</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; }
canvas { display: block; border: 1.5px solid #adb1b8; border-radius: 6px;
cursor: crosshair; touch-action: none; }
.controls { display: flex; gap: .5rem; margin: .6rem 0 .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border-radius: 7px;
border: 1.5px solid #1d3557; cursor: pointer; }
button:not(.ghost) { background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .85rem; font-weight: 600; color: #555; margin-bottom: .5rem; min-height: 1.2em; }
.output-wrap { border: 1px solid #cdd9e3; border-radius: 6px; padding: .5rem .7rem;
background: #f7f9fb; max-height: 160px; overflow-y: auto; }
.output-label { font-size: .78rem; font-weight: 700; color: #888; margin-bottom: .3rem;
letter-spacing: .04em; text-transform: uppercase; }
.output { font: 13px/1.7 ui-monospace, monospace; color: #1d3557; word-break: break-all; }
.run-w { color: #888; }
.run-b { color: #1d3557; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Run-Length Encoding demo.
// Grid: COLS x ROWS cells. Each cell is 0 (white) or 1 (black).
// encode(row): produce (symbol,count) pairs scanning left to right.
// The demo also tracks raw pixel count vs encoded pair count to show the compression ratio live.
const COLS = 16, ROWS = 16;
const CELL = 20; // px per cell
const grid = new Array(ROWS).fill(null).map(() => new Array(COLS).fill(0));
const canvas = document.getElementById('grid');
const ctx = canvas.getContext('2d');
canvas.width = COLS * CELL;
canvas.height = ROWS * CELL;
// โโ RLE encode a single row โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function encodeRow(row) {
if (!row.length) return [];
const pairs = [];
let cur = row[0], cnt = 1;
for (let i = 1; i < row.length; i++) {
if (row[i] === cur) { cnt++; }
else { pairs.push([cur, cnt]); cur = row[i]; cnt = 1; }
}
pairs.push([cur, cnt]);
return pairs;
}
// โโ Encode the full grid and update UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateOutput() {
const outputEl = document.getElementById('output');
const statsEl = document.getElementById('stats');
let totalRaw = 0, totalPairs = 0;
const lines = [];
for (let r = 0; r < ROWS; r++) {
const pairs = encodeRow(grid[r]);
totalRaw += COLS;
totalPairs += pairs.length;
const spans = pairs.map(([sym, cnt]) => {
const cls = sym === 0 ? 'run-w' : 'run-b';
const label = sym === 0 ? 'W' : 'B';
return `<span class="${cls}">(${label},${cnt})</span>`;
});
lines.push(fmt('Row %r%%v%: %spans%', { r: r, v: 1, spans: spans.join(' ') }));
}
outputEl.innerHTML = lines.join('<br>');
const ratio = (totalPairs * 2 / totalRaw).toFixed(2);
const pct = Math.round((1 - totalPairs * 2 / totalRaw) * 100);
statsEl.textContent =
fmt('Raw: %totalRaw% pixels โ Encoded: %totalPairs% pairs (%v% values) | %v2%', { totalRaw: totalRaw, totalPairs: totalPairs, v: totalPairs * 2, v2: pct >= 0 ? 'Saved ' + pct + '%' : 'Expanded ' + (-pct) + '%' });
// Log for node verification
console.log('VERIFY encodeRow([1,1,0,0,0,1]):', JSON.stringify(encodeRow([1,1,0,0,0,1])));
console.log('VERIFY encodeRow([0,1,0,1,0,1]):', JSON.stringify(encodeRow([0,1,0,1,0,1])));
console.log('VERIFY encodeRow([0,0,0,0,0,0]):', JSON.stringify(encodeRow([0,0,0,0,0,0])));
}
// โโ Draw the canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
ctx.fillStyle = grid[r][c] === 1 ? '#1d3557' : '#f0f4f8';
ctx.fillRect(c * CELL, r * CELL, CELL, CELL);
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 0.5;
ctx.strokeRect(c * CELL + 0.25, r * CELL + 0.25, CELL - 0.5, CELL - 0.5);
}
}
updateOutput();
}
// โโ Mouse / touch input โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let painting = false, paintVal = 0;
function cellAt(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const c = Math.floor(x / CELL);
const r = Math.floor(y / CELL);
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) return [r, c];
return null;
}
canvas.addEventListener('mousedown', e => {
const pos = cellAt(e);
if (!pos) return;
painting = true;
paintVal = grid[pos[0]][pos[1]] === 0 ? 1 : 0;
grid[pos[0]][pos[1]] = paintVal;
draw();
});
canvas.addEventListener('mousemove', e => {
if (!painting) return;
const pos = cellAt(e);
if (pos) { grid[pos[0]][pos[1]] = paintVal; draw(); }
});
window.addEventListener('mouseup', () => { painting = false; });
canvas.addEventListener('touchstart', e => {
e.preventDefault();
const pos = cellAt(e.touches[0]);
if (!pos) return;
painting = true;
paintVal = grid[pos[0]][pos[1]] === 0 ? 1 : 0;
grid[pos[0]][pos[1]] = paintVal;
draw();
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
if (!painting) return;
const pos = cellAt(e.touches[0]);
if (pos) { grid[pos[0]][pos[1]] = paintVal; draw(); }
}, { passive: false });
window.addEventListener('touchend', () => { painting = false; });
// โโ Preset buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('clear').addEventListener('click', () => {
for (let r = 0; r < ROWS; r++) grid[r].fill(0);
draw();
});
document.getElementById('fill').addEventListener('click', () => {
for (let r = 0; r < ROWS; r++) grid[r].fill(1);
draw();
});
document.getElementById('checker').addEventListener('click', () => {
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
grid[r][c] = (r + c) % 2;
draw();
});
// โโ Initial pattern: a simple house silhouette โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// roof (rows 2-5 pyramid), walls (rows 6-13), door (rows 10-13 center)
(function initPattern() {
// Roof
[[2,7,8],[3,6,9],[4,5,10],[5,4,11]].forEach(([row,cL,cR]) => {
for (let c = cL; c <= cR; c++) grid[row][c] = 1;
});
// Walls
for (let r = 6; r <= 13; r++)
for (let c = 4; c <= 11; c++) grid[r][c] = 1;
// Door opening
for (let r = 10; r <= 13; r++)
for (let c = 7; c <= 8; c++) grid[r][c] = 0;
draw();
})();
Notice how long unbroken stretches collapse to a single pair, while a checkerboard pattern actually grows โ every pixel becomes its own run of length 1, and the encoding is twice as long as the original. That is the fundamental trade-off: RLE shines on data with large runs, and fails on data with high alternation.
Comments
Loading comments...