Compare commits
5 commits
260f9c53ed
...
88bb31fb43
Author | SHA1 | Date | |
---|---|---|---|
88bb31fb43 | |||
605e6d04b9 | |||
555a34b4db | |||
ca7faf698f | |||
77aeccccef |
1 changed files with 77 additions and 78 deletions
155
index.html
155
index.html
|
@ -22,18 +22,14 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Currently just converts to grayscale and counts up unique blocks.
|
Currently converts to grayscale and reuses existing similar blocks as it goes,
|
||||||
Next step: decimate if > 128 unique blocks per image, and combine
|
increasing a similarity threshold (from 0) until the set fits in 128 chars.
|
||||||
the most similar blocks in the output.
|
Next step: correctly handle inverse video similarities
|
||||||
|
Further step: don't end up stuck between 64 and 128 blocks :D
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Source video</h2>
|
|
||||||
<div>
|
<div>
|
||||||
<video id="source" src="llamigos.webm" class="stretchy" muted controls playsinline></video>
|
<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="work" width="80" height="160" class="stretchy"></canvas>
|
||||||
<canvas id="font" width="80" height="160" class="stretchy"></canvas>
|
<canvas id="font" width="80" height="160" class="stretchy"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,8 +69,50 @@
|
||||||
return Array.from(block).map((n) => n.toString(16)).join('');
|
return Array.from(block).map((n) => n.toString(16)).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function inverse(block) {
|
function inverse(pixel) {
|
||||||
return block.map((n) => ~n & 0xf);
|
return ~pixel & 0xf;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchBlocks(a, b, threshold) {
|
||||||
|
for (let i = 0; i < blockWidth * blockHeight; i++) {
|
||||||
|
if (Math.abs(a[i] - b[i]) > threshold) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchBlocksInverse(a, b, threshold) {
|
||||||
|
for (let i = 0; i < blockWidth * blockHeight; i++) {
|
||||||
|
if (Math.abs(a[i] - inverse(b[i])) > threshold) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChar(imageData, cx, cy, char, charset) {
|
||||||
|
let invert = Boolean(char & 0x80);
|
||||||
|
char &= 0x7f;
|
||||||
|
if (char >= charset.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let block = charset[char];
|
||||||
|
for (let y = 0; y < blockHeight; y++) {
|
||||||
|
for (let x = 0; x < blockWidth; x++) {
|
||||||
|
let i = y * blockWidth + x;
|
||||||
|
let ii = (y + cy * blockHeight) * imageData.width + (x + cx * blockWidth);
|
||||||
|
let gray16 = block[i];
|
||||||
|
if (invert) {
|
||||||
|
gray16 = inverse(gray16);
|
||||||
|
}
|
||||||
|
let gray256 = Math.round(gray16 * 255 / 15);
|
||||||
|
imageData.data[ii * 4] = gray256;
|
||||||
|
imageData.data[ii * 4 + 1] = gray256;
|
||||||
|
imageData.data[ii * 4 + 2] = gray256;
|
||||||
|
imageData.data[ii * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
|
@ -121,35 +159,12 @@
|
||||||
//
|
//
|
||||||
// First pass: uniques extraction
|
// First pass: uniques extraction
|
||||||
// Convert the 4bpp pixel indices into hex strings
|
// Convert the 4bpp pixel indices into hex strings
|
||||||
let blockMap = {};
|
|
||||||
let uniques = [];
|
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++) {
|
for (let threshold = 0; threshold < 16; threshold++) {
|
||||||
charIter:
|
charIter:
|
||||||
for (let i = 0; i < chars.length; i++) {
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
let char = chars[i];
|
let block = blocks[i];
|
||||||
let block = blocks[char];
|
|
||||||
if (!block) {
|
|
||||||
debugger
|
|
||||||
throw new Error('missing block');
|
|
||||||
}
|
|
||||||
|
|
||||||
fontMatch:
|
fontMatch:
|
||||||
for (let j = 0; j < uniques.length; j++) {
|
for (let j = 0; j < uniques.length; j++) {
|
||||||
|
@ -158,14 +173,16 @@
|
||||||
debugger
|
debugger
|
||||||
throw new Error('missing other');
|
throw new Error('missing other');
|
||||||
}
|
}
|
||||||
for (let k = 0; k < blockWidth * blockHeight; k++) {
|
if (matchBlocks(block, other, threshold)) {
|
||||||
if (Math.abs(block[k] - other[k]) > threshold) {
|
// we're close enough to reuse a character
|
||||||
continue fontMatch;
|
chars[i] = j;
|
||||||
}
|
continue charIter;
|
||||||
|
} else if (matchBlocksInverse(block, other, threshold)) {
|
||||||
|
chars[i] = j | 0x8000;
|
||||||
|
continue charIter;
|
||||||
|
} else {
|
||||||
|
continue fontMatch;
|
||||||
}
|
}
|
||||||
// we're close enough to reuse a character
|
|
||||||
chars[i] = j;
|
|
||||||
continue charIter;
|
|
||||||
}
|
}
|
||||||
// add a new char
|
// add a new char
|
||||||
chars[i] = uniques.push(block) - 1;
|
chars[i] = uniques.push(block) - 1;
|
||||||
|
@ -174,55 +191,34 @@
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// We need to decimate further
|
// We need to decimate further
|
||||||
blocks = uniques;
|
|
||||||
uniques = [];
|
uniques = [];
|
||||||
}
|
}
|
||||||
let span = document.querySelector('#block-count');
|
let span = document.querySelector('#block-count');
|
||||||
span.textContent = `${uniques.length}`;
|
span.textContent = `${uniques.length}`;
|
||||||
|
|
||||||
// Font (currently wrong! :D)
|
// Font
|
||||||
let fontCtx = document.querySelector('#font').getContext('2d');
|
let fontCtx = document.querySelector('#font').getContext('2d');
|
||||||
let font = fontCtx.createImageData(16 * blockWidth, 16 * blockHeight);
|
let font = fontCtx.createImageData(16 * blockWidth, 16 * blockHeight);
|
||||||
for (let hi = 0; hi < 16; hi++) {
|
for (let hi = 0; hi < 16; hi++) {
|
||||||
for (let lo = 0; lo < 16; lo++) {
|
for (let lo = 0; lo < 16; lo++) {
|
||||||
let char = (hi << 4) | lo;
|
let char = (hi << 4) | lo;
|
||||||
let invert = Boolean(char & 0x80);
|
drawChar(font, lo, hi, char, uniques);
|
||||||
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);
|
fontCtx.putImageData(font, 0, 0);
|
||||||
|
|
||||||
// Redraw the blocks
|
// Redraw the blocks
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
for (let cy = 0; cy < heightBlocks; cy++) {
|
||||||
let i = y * width + x;
|
for (let cx = 0; cx < widthBlocks; cx++) {
|
||||||
let gray16 = pixels[i];
|
let i = cy * widthBlocks + cx;
|
||||||
let gray256 = Math.round(gray16 * 255 / 15);
|
let char = chars[i];
|
||||||
data[i * 4] = gray256;
|
if (char & 0x8000) {
|
||||||
data[i * 4 + 1] = gray256;
|
// we use a bigger bit during earlier stages
|
||||||
data[i * 4 + 2] = gray256;
|
char &= 0x7f;
|
||||||
|
char |= 0x80;
|
||||||
|
}
|
||||||
|
drawChar(bits, cx, cy, char, uniques);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.putImageData(bits, 0, 0);
|
ctx.putImageData(bits, 0, 0);
|
||||||
|
@ -231,7 +227,10 @@
|
||||||
let timer = null;
|
let timer = null;
|
||||||
source.addEventListener('playing', () => {
|
source.addEventListener('playing', () => {
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
timer = setInterval(update, 1000 / 10);
|
// target 8 fps
|
||||||
|
// not sure we can get any faster
|
||||||
|
// downloads over sio
|
||||||
|
timer = setInterval(update, 1000 / 8);
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue