Created
June 6, 2026 12:31
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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×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 → 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 → 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> → ` + | |
| `L=${L} R=${R} G=${G} B=${B} → ` + | |
| `<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