Skip to content

Instantly share code, notes, and snippets.

@ped7g
Created June 6, 2026 12:31
Show Gist options
  • Select an option

  • Save ped7g/adc5dfb3011d92e1abfcdd10d6e05a86 to your computer and use it in GitHub Desktop.

Select an option

Save ped7g/adc5dfb3011d92e1abfcdd10d6e05a86 to your computer and use it in GitHub Desktop.
Explorer of idea of 2 bits per RGB channel with remaining 2 bits for luminosity, code by GenAI
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>2:2:2 + 2-bit luminance palette explorer</title>
<style>
:root { color-scheme: dark; }
body {
margin: 0; padding: 16px;
background: #1e1e1e; color: #ddd;
font: 13px/1.4 system-ui, sans-serif;
}
h1 { font-size: 16px; margin: 0 0 4px; }
p.sub { margin: 0 0 12px; color: #999; }
.controls {
display: grid;
grid-template-columns: 256px 1fr;
gap: 12px 24px;
align-items: start; margin-bottom: 16px;
background: #262626; padding: 12px 16px; border-radius: 8px;
}
.controls label { display: block; margin-bottom: 4px; color: #bbb; }
.controls select, .controls input { font: inherit; }
#formula { max-width: 60ch; color: #8c8; font: 11px/1.4 monospace; white-space: pre-line; }
canvas { image-rendering: pixelated; border: 1px solid #000; background: #000; }
#info { color: #aaa; min-height: 1.4em; font-variant-numeric: tabular-nums; }
code { color: #6cf; }
</style>
</head>
<body>
<h1>2:2:2 RGB + 2-bit luminance palette explorer</h1>
<p class="sub">Byte layout <code>LLRRGGBB</code> (2 bits luminance, 2 each R/G/B). 16&times;16 = 256 colors, expanded to 24-bit "true color".</p>
<div class="controls">
<div>
<label><input type="checkbox" id="showHex" checked> show hex label</label>
</div>
<div id="info">Hover a cell for details.</div>
<div>
<label for="mode">Luminance &rarr; color mapping</label>
<select id="mode">
<option value="scaleMin">Scale (L dims color, 25% min)</option>
<option value="scaleZero">Scale (L dims color, L=0 &rarr; black)</option>
<option value="rep_cocolulu">Replicate: cocolulu</option>
<option value="rep_colucolu">Replicate: colucolu</option>
<option value="rep_clouclou">Replicate: clouclou</option>
<option value="rep_cococolu">Replicate: cococolu</option>
<option value="ambient">Additive ambient (L lifts toward gray)</option>
</select>
</div>
<div id="formula"></div>
</div>
<canvas id="cv" width="512" height="512"></canvas>
<script>
"use strict";
const CELL = 32, GAP = 1, COLS = 16, ROWS = 16;
const cv = document.getElementById("cv");
const ctx = cv.getContext("2d");
const info = document.getElementById("info");
const els = {
mode: document.getElementById("mode"),
showHex: document.getElementById("showHex"),
};
const formulaEl = document.getElementById("formula");
// One color channel = 2 bits "co" = [c1 c0]; shared luminance = 2 bits "lu" = [l1 l0].
// Each replicate pattern is an 8-entry list of those single bits (high -> low).
const PATTERNS = {
cocolulu: ["c1","c0","c1","c0","l1","l0","l1","l0"],
colucolu: ["c1","c0","l1","l0","c1","c0","l1","l0"],
clouclou: ["c1","l1","c0","l0","c1","l1","c0","l0"],
cococolu: ["c1","c0","c1","c0","c1","c0","l1","l0"],
};
// Terse note shown under the combo describing how 24-bit values are built.
const FORMULAS = {
scaleMin: "out = round(255 * co/3 * (L+1)/4) per channel\nL only dims; L=0 keeps 25% of the color.",
scaleZero: "out = round(255 * co/3 * L/3) per channel\nL=0 forces black for every color.",
rep_cocolulu: "8-bit channel = co co lu lu\ncolor in high nibble, luminance in low nibble.",
rep_colucolu: "8-bit channel = co lu co lu\ncolor & luminance bit-pairs interleaved.",
rep_clouclou: "8-bit channel = c1 l1 c0 l0 x2\nsingle color/lum bits interleaved, then doubled.",
rep_cococolu: "8-bit channel = co co co lu\ncolor repeated x3, luminance in lowest pair.",
ambient: "floor = 255 * L/3 * 0.5 ; out = floor + (255-floor)*co/3\nL raises a gray floor instead of darkening.",
};
// Build an 8-bit channel value from 2-bit color "co" and 2-bit luminance "lu".
function packChannel(co, lu, pattern) {
const bit = {
c1: (co >> 1) & 1, c0: co & 1,
l1: (lu >> 1) & 1, l0: lu & 1,
};
let out = 0;
for (const t of pattern) out = (out << 1) | bit[t];
return out;
}
// Decode one LLRRGGBB byte, return {r,g,b} 0..255 plus the raw fields.
function decode(byte, mode) {
const L = (byte >> 6) & 3;
const R = (byte >> 4) & 3;
const G = (byte >> 2) & 3;
const B = byte & 3;
let r, g, b;
if (mode.startsWith("rep_")) {
const p = PATTERNS[mode.slice(4)];
r = packChannel(R, L, p);
g = packChannel(G, L, p);
b = packChannel(B, L, p);
} else if (mode === "scaleMin") {
const f = (L + 1) / 4;
r = Math.round(255 * (R / 3) * f);
g = Math.round(255 * (G / 3) * f);
b = Math.round(255 * (B / 3) * f);
} else if (mode === "scaleZero") {
const f = L / 3;
r = Math.round(255 * (R / 3) * f);
g = Math.round(255 * (G / 3) * f);
b = Math.round(255 * (B / 3) * f);
} else { // ambient
const floor = Math.round(255 * (L / 3) * 0.5);
r = Math.min(255, floor + Math.round((255 - floor) * (R / 3)));
g = Math.min(255, floor + Math.round((255 - floor) * (G / 3)));
b = Math.min(255, floor + Math.round((255 - floor) * (B / 3)));
}
return { r, g, b, L, R, G, B };
}
function contrastColor(r, g, b) {
// Perceived luminance (Rec. 601); pick black or white text for contrast.
const y = 0.299 * r + 0.587 * g + 0.114 * b;
return y > 140 ? "#000" : "#fff";
}
function draw() {
const mode = els.mode.value;
const showHex = els.showHex.checked;
formulaEl.textContent = FORMULAS[mode] || "";
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, cv.width, cv.height);
for (let byte = 0; byte < 256; byte++) {
const col = byte % COLS, row = (byte / COLS) | 0;
const x = col * CELL + GAP, y = row * CELL + GAP;
const size = CELL - GAP;
const { r, g, b } = decode(byte, mode);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, size, size);
if (showHex) {
ctx.fillStyle = contrastColor(r, g, b);
ctx.font = "9px monospace";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
ctx.fillText(byte.toString(16).toUpperCase().padStart(2, "0"),
x + size - 2, y + size - 1);
}
}
}
cv.addEventListener("mousemove", (e) => {
const rect = cv.getBoundingClientRect();
const px = (e.clientX - rect.left) * cv.width / rect.width;
const py = (e.clientY - rect.top) * cv.height / rect.height;
const col = Math.floor(px / CELL), row = Math.floor(py / CELL);
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return;
const byte = row * COLS + col;
const { r, g, b, L, R, G, B } = decode(byte, els.mode.value);
const hex = byte.toString(16).toUpperCase().padStart(2, "0");
const bin = byte.toString(2).padStart(8, "0");
info.innerHTML =
`byte <code>$${hex}</code> = <code>%${bin}</code> &rarr; ` +
`L=${L} R=${R} G=${G} B=${B} &rarr; ` +
`<code>#${[r,g,b].map(v=>v.toString(16).padStart(2,"0")).join("").toUpperCase()}</code> ` +
`rgb(${r}, ${g}, ${b})`;
});
for (const el of Object.values(els)) {
el.addEventListener("input", draw);
el.addEventListener("change", draw);
}
draw();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment