<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Block video encoding test</title>
        <style type="text/css">
            .stretchy {
                width: 320px;
                height: 192px;
                object-fit: fill;
                image-rendering: pixelated;
            }
        </style>
    </head>
    <body>
        <h1>Block video encoding test</h1>

        <p>
            Test work for a video encoding using Atari 800-family GTIA grayscale mode on top of text cells.
            This allows a "block dictionary" of 128 2x8-pixel blocks on an 80x160 grayscale image,
            at cost of 1840 bytes per full frame or 800 bytes to reuse previous blocks.
        </p>

        <p>
            Currently just converts to grayscale and counts up unique blocks.
            Next step: decimate if &gt; 128 unique blocks per image, and combine
            the most similar blocks in the output.
        </p>

        <h2>Source video</h2>
        <div>
            <video id="source" src="llamigos.webm" class="stretchy" muted controls playsinline></video>
        </div>

        <h2>Work canvas</h2>
        <div>
            <canvas id="work" width="80" height="160" class="stretchy"></canvas>
            <canvas id="font" width="80" height="160" class="stretchy"></canvas>
        </div>
        <div>
            <span id="block-count">n/a</span> unique blocks per frame
        </div>

        <script type="text/javascript">
            let width = 80;
            let height = 160;

            let blockWidth = 2;
            let blockHeight = 8;

            let widthBlocks = width / blockWidth;
            let heightBlocks = height / blockHeight;

            function fromSRGB(val) {
                if (val <= 0.04045) {
                    return val / 12.92;
                }
                return ((val + 0.055) / 1.055) ** 2.4;
            }

            function toSRGB(val) {
                if (val <= 0.0031308) {
                    return val * 12.92;
                }
                return (val * 1.055) ** (1.0 / 2.4) - 0.055;
            }

            function luma(r, g, b) {
                return r * 0.299 + g * 0.586 + b * 0.114;
            }

            function hexify(block) {
                return Array.from(block).map((n) => n.toString(16)).join('');
            }

            function inverse(block) {
                return block.map((n) => ~n & 0xf);
            }

            function update() {
                let ctx = work.getContext('2d');
                let pixels = new Uint8Array(width * height);

                // Extract the luma
                ctx.drawImage(source, 0, 0);
                let bits = ctx.getImageData(0, 0, width, height);
                let data = bits.data;
                for (let y = 0; y < height; y++) {
                    for (let x = 0; x < width; x++) {
                        let i = y * width + x;

                        let r = fromSRGB(data[i * 4] / 255);
                        let g = fromSRGB(data[i * 4 + 1] / 255);
                        let b = fromSRGB(data[i * 4 + 2] / 255);

                        let grayLinear = luma(r, g, b);
                        let gray = toSRGB(grayLinear);
                        let gray16 = Math.round(gray * 15);
                        pixels[i] = gray16;
                    }
                }

                let blocks = [];
                let chars = new Uint16Array(widthBlocks * heightBlocks);
                for (y = 0; y < heightBlocks; y++) {
                    for (let x = 0; x < widthBlocks; x++) {
                        let i = y * widthBlocks + x;
                        blocks[i] = new Uint8Array(blockWidth * blockHeight);
                        chars[i] = i;
                        for (let yy = 0; yy < blockHeight; yy++) {
                            for (let xx = 0; xx < blockWidth; xx++) {
                                let ii = yy * blockWidth + xx;
                                blocks[i][ii] = pixels[(y * blockHeight + yy) * width + (x * blockWidth + xx)];
                            }
                        }
                    }
                }

                // Now we have 800 blocks for 80x160 image
                // But we can only use 128 + their mirror images
                //
                // First pass: uniques extraction
                // Convert the 4bpp pixel indices into hex strings
                let blockMap = {};
                let uniques = [];
                /*
                for (let i = 0; i < chars.length; i++) {
                    let char = chars[i];
                    let block = blocks[char];
                    let key = hexify(blocks[i]);
                    let keyInverse = hexify(inverse(block));
                    if (blockMap[key]) {
                        char = blockMap[key];
                    } else if (blockMap[keyInverse]) {
                        char = blockMap[keyInverse];
                    } else {
                        char = uniques.push(block) - 1;
                        blockMap[key] = char;
                        blockMap[keyInverse] = char;
                    }
                    chars[i] = char;
                }
                */
                for (let threshold = 0; threshold < 16; threshold++) {
                    charIter:
                    for (let i = 0; i < chars.length; i++) {
                        let char = chars[i];
                        let block = blocks[char];
                        if (!block) {
                            debugger
                            throw new Error('missing block');
                        }

                        fontMatch:
                        for (let j = 0; j < uniques.length; j++) {
                            let other = uniques[j];
                            if (!block) {
                                debugger
                                throw new Error('missing other');
                            }
                            for (let k = 0; k < blockWidth * blockHeight; k++) {
                                if (Math.abs(block[k] - other[k]) > threshold) {
                                    continue fontMatch;
                                }
                            }
                            // we're close enough to reuse a character
                            chars[i] = j;
                            continue charIter;
                        }
                        // add a new char
                        chars[i] = uniques.push(block) - 1;
                    }
                    if (uniques.length < 128) {
                        break;
                    }
                    // We need to decimate further
                    blocks = uniques;
                    uniques = [];
                }
                let span = document.querySelector('#block-count');
                span.textContent = `${uniques.length}`;

                // Font (currently wrong! :D)
                let fontCtx = document.querySelector('#font').getContext('2d');
                let font = fontCtx.createImageData(16 * blockWidth, 16 * blockHeight);
                for (let hi = 0; hi < 16; hi++) {
                    for (let lo = 0; lo < 16; lo++) {
                        let char = (hi << 4) | lo;
                        let invert = Boolean(char & 0x80);
                        char &= 0x7f;
                        if (char >= uniques.length) {
                            continue;
                        }
                        let block = uniques[char];
                        for (let y = 0; y < blockHeight; y++) {
                            for (let x = 0; x < blockWidth; x++) {
                                let i = y * blockWidth + x;
                                let ii = (y + hi * blockHeight) * 16 * blockWidth + (x + lo * blockWidth);
                                if (block.length < i) {
                                    debugger;
                                }
                                let gray16 = block[i];
                                if (invert) {
                                    gray16 = ~gray16 & 0x0f;
                                }
                                let gray256 = Math.round(gray16 * 255 / 15);
                                font.data[ii * 4] = gray256;
                                font.data[ii * 4 + 1] = gray256;
                                font.data[ii * 4 + 2] = gray256;
                                font.data[ii * 4 + 3] = 255;
                            }
                        }
                    }
                }
                fontCtx.putImageData(font, 0, 0);

                // Redraw the blocks
                for (let y = 0; y < height; y++) {
                    for (let x = 0; x < width; x++) {
                        let i = y * width + x;
                        let gray16 = pixels[i];
                        let gray256 = Math.round(gray16 * 255 / 15);
                        data[i * 4] = gray256;
                        data[i * 4 + 1] = gray256;
                        data[i * 4 + 2] = gray256;
                    }
                }
                ctx.putImageData(bits, 0, 0);
            }

            let timer = null;
            source.addEventListener('playing', () => {
                if (!timer) {
                    timer = setInterval(update, 1000 / 10);
                }
                update();
            });
            source.addEventListener('pause', () => {
                if (timer) {
                    clearInterval(timer);
                    timer = null;
                }
            });
        </script>
    </body>
</html>