Skip to content

Commit

Permalink
Unicode 16 based 4x2 pixel mode
Browse files Browse the repository at this point in the history
  • Loading branch information
matushorvath committed Jun 28, 2024
1 parent 6246fde commit 290acba
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
9 changes: 9 additions & 0 deletions video/octants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
process.stdout.write('\x1b[93;100m');

//\x1b[38;2;${c24[f].map(x => x.toString()).join(';')};48;2;${c24[b].map(x => x.toString()).join(';')}m

for (let i = 0x00; i < 0xe5; i++) {
process.stdout.write(String.fromCodePoint(0x1CD00 + i) + 'โ–ˆ');
}

process.stdout.write('\x1b[0m');
135 changes: 135 additions & 0 deletions video/xterm-2x2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import sharp from 'sharp';
import 'promise.withresolvers/auto';

const keypress = async () => {
const { resolve, _reject, promise } = Promise.withResolvers();

process.stdin.setRawMode(true);
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
resolve();
});

return promise;
};

const saveCursor = () => process.stdout.write('\x1b7');
const restoreCursor = () => process.stdout.write('\x1b8');
const setCursor = (row, col) => process.stdout.write(`\x1b[${row};${col}H`);
const clearDisplay = () => process.stdout.write('\x1b[2J');

const alternateBuffer = () => process.stdout.write('\x1b[?47h');
const normalBuffer = () => process.stdout.write('\x1b[?47l');

//const insertMode = () => process.stdout.write('\x1b[4h');
//const replaceMode = () => process.stdout.write('\x1b[4l');

//const autoNewline = () => process.stdout.write('\x1b[20h');
//const normalNewline = () => process.stdout.write('\x1b[20l');

const hideCursor = () => process.stdout.write('\x1b[?25l');
const showCursor = () => process.stdout.write('\x1b[?25h');

const setForeground = (r, g, b) => process.stdout.write(`\x1b[38;2;${r};${g};${b}m`);
const setBackground = (r, g, b) => process.stdout.write(`\x1b[48;2;${r};${g};${b}m`);
const resetColor = () => process.stdout.write('\x1b[0m');

class Frame {
constructor(cols, rows, data) {
this.cols = cols;
this.rows = rows;
this.data = data;
}

static CBL = 2;
static RBL = 2;

display() {
// TODO handle images with size not divisible by CBL, RBL
const cblocks = Math.ceil(this.cols / Frame.CBL);
const rblocks = Math.ceil(this.rows / Frame.RBL);

for (let r = 0; r < rblocks; r++) {
for (let c = 0; c < cblocks; c++) {
const pixels = this.getBlockPixels(c, r);
const { char, foreground, background } = this.getBlockChar(pixels);

setCursor(r + 1, c + 1);

setForeground(foreground.r, foreground.g, foreground.b);
setBackground(background.r, background.g, background.b);

process.stdout.write(char);

resetColor();
}
}
}

calcIndex(bc, br, dc, dr) {
return 3 * ((br * Frame.RBL + dr) * this.cols + (bc * Frame.CBL + dc));
}

getPixel(bc, br, dc, dr) {
const index = this.calcIndex(bc, br, dc, dr);
return { r: this.data[index + 0] ?? 0, g: this.data[index + 1] ?? 0, b: this.data[index + 2] ?? 0 };
}

getBlockPixels(c, r) {
const res = [];
for (let dc = 0; dc < Frame.CBL; dc++) {
for (let dr = 0; dr < Frame.RBL; dr++) {
res.push(this.getPixel(c, r, dc, dr));
}
}
return res;
}

static BLOCKS_2x2 = [
' ', 'โ–˜', 'โ––', 'โ–Œ',
'โ–', 'โ–€', 'โ–ž', 'โ–›',
'โ–—', 'โ–š', 'โ–„', 'โ–™',
'โ–', 'โ–œ', 'โ–Ÿ', 'โ–ˆ'
];

getBlockChar(pixels) {
const vals = [];
for (const { r, g, b } of pixels) {
vals.push((r + g + b >= (3 * 0xc0)) ? 1 : 0);
}

return {
char: Frame.BLOCKS_2x2[Number.parseInt(vals.reverse().join(''), 2)],
foreground: { r: 0xff, g: 0xff, b: 0xff },
background: { r: 0x00, g: 0x00, b: 0x00 }
};
}
}

const main = async () => {
const cols = 320, rows = 200;

const image = await sharp('sammy.png')
.resize(cols, rows, { fit: 'inside' })
.toColorspace('srgb');
//await image.toFile('delme.png');
const buffer = await image.raw().toBuffer({ resolveWithObject: true });

const frame = new Frame(buffer.info.width, buffer.info.height, buffer.data);

hideCursor();
saveCursor();
alternateBuffer();
clearDisplay();

frame.display();

await keypress();

normalBuffer();
restoreCursor();
showCursor();
};

await main();
File renamed without changes.
152 changes: 152 additions & 0 deletions video/xterm-4x2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import sharp from 'sharp';
import 'promise.withresolvers/auto';

const keypress = async () => {
const { resolve, _reject, promise } = Promise.withResolvers();

process.stdin.setRawMode(true);
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
resolve();
});

return promise;
};

const saveCursor = () => process.stdout.write('\x1b7');
const restoreCursor = () => process.stdout.write('\x1b8');
const setCursor = (row, col) => process.stdout.write(`\x1b[${row};${col}H`);
const clearDisplay = () => process.stdout.write('\x1b[2J');

const alternateBuffer = () => process.stdout.write('\x1b[?47h');
const normalBuffer = () => process.stdout.write('\x1b[?47l');

//const insertMode = () => process.stdout.write('\x1b[4h');
//const replaceMode = () => process.stdout.write('\x1b[4l');

//const autoNewline = () => process.stdout.write('\x1b[20h');
//const normalNewline = () => process.stdout.write('\x1b[20l');

const hideCursor = () => process.stdout.write('\x1b[?25l');
const showCursor = () => process.stdout.write('\x1b[?25h');

const setForeground = (r, g, b) => process.stdout.write(`\x1b[38;2;${r};${g};${b}m`);
const setBackground = (r, g, b) => process.stdout.write(`\x1b[48;2;${r};${g};${b}m`);
const resetColor = () => process.stdout.write('\x1b[0m');

class Frame {
constructor(cols, rows, data) {
this.cols = cols;
this.rows = rows;
this.data = data;
}

static CBL = 2;
static RBL = 4;

display() {
// TODO handle images with size not divisible by CBL, RBL
const cblocks = Math.ceil(this.cols / Frame.CBL);
const rblocks = Math.ceil(this.rows / Frame.RBL);

for (let r = 0; r < rblocks; r++) {
for (let c = 0; c < cblocks; c++) {
const pixels = this.getBlockPixels(c, r);
const { char, foreground, background } = this.getBlockChar(pixels);

setCursor(r + 1, c + 1);

setForeground(foreground.r, foreground.g, foreground.b);
setBackground(background.r, background.g, background.b);

process.stdout.write(char);

resetColor();
}
}
}

calcIndex(bc, br, dc, dr) {
return 3 * ((br * Frame.RBL + dr) * this.cols + (bc * Frame.CBL + dc));
}

getPixel(bc, br, dc, dr) {
const index = this.calcIndex(bc, br, dc, dr);
return { r: this.data[index + 0] ?? 0, g: this.data[index + 1] ?? 0, b: this.data[index + 2] ?? 0 };
}

getBlockPixels(c, r) {
const res = [];
for (let dr = 0; dr < Frame.RBL; dr++) {
for (let dc = 0; dc < Frame.CBL; dc++) {
res.push(this.getPixel(c, r, dc, dr));
}
}
return res;
}

// octants (2ร—4 mosaics) U+1CD00 - U+1CDE5
// + U+1FBE6 and U+1FBE7 two new quadrants
// https://gist.github.com/Explorer09/1da382e4b1cf3bf2e8009e60836af70b
// http://www.unicode.org/L2/L2021/21235-terminals-supplement.pdf

static BLOCKS_4x2 = [
'ย ', '๐œบจ', '๐œบซ', '๐Ÿฎ‚', '๐œด€', 'โ–˜', '๐œด', '๐œด‚', '๐œดƒ', '๐œด„', 'โ–', '๐œด…', '๐œด†', '๐œด‡', '๐œดˆ', 'โ–€',
'๐œด‰', '๐œดŠ', '๐œด‹', '๐œดŒ', '๐Ÿฏฆ', '๐œด', '๐œดŽ', '๐œด', '๐œด', '๐œด‘', '๐œด’', '๐œด“', '๐œด”', '๐œด•', '๐œด–', '๐œด—',
'๐œด˜', '๐œด™', '๐œดš', '๐œด›', '๐œดœ', '๐œด', '๐œดž', '๐œดŸ', '๐Ÿฏง', '๐œด ', '๐œดก', '๐œดข', '๐œดฃ', '๐œดค', '๐œดฅ', '๐œดฆ',
'๐œดง', '๐œดจ', '๐œดฉ', '๐œดช', '๐œดซ', '๐œดฌ', '๐œดญ', '๐œดฎ', '๐œดฏ', '๐œดฐ', '๐œดฑ', '๐œดฒ', '๐œดณ', '๐œดด', '๐œดต', '๐Ÿฎ…',
'๐œบฃ', '๐œดถ', '๐œดท', '๐œดธ', '๐œดน', '๐œดบ', '๐œดป', '๐œดผ', '๐œดฝ', '๐œดพ', '๐œดฟ', '๐œต€', '๐œต', '๐œต‚', '๐œตƒ', '๐œต„',
'โ––', '๐œต…', '๐œต†', '๐œต‡', '๐œตˆ', 'โ–Œ', '๐œต‰', '๐œตŠ', '๐œต‹', '๐œตŒ', 'โ–ž', '๐œต', '๐œตŽ', '๐œต', '๐œต', 'โ–›',
'๐œต‘', '๐œต’', '๐œต“', '๐œต”', '๐œต•', '๐œต–', '๐œต—', '๐œต˜', '๐œต™', '๐œตš', '๐œต›', '๐œตœ', '๐œต', '๐œตž', '๐œตŸ', '๐œต ',
'๐œตก', '๐œตข', '๐œตฃ', '๐œตค', '๐œตฅ', '๐œตฆ', '๐œตง', '๐œตจ', '๐œตฉ', '๐œตช', '๐œตซ', '๐œตฌ', '๐œตญ', '๐œตฎ', '๐œตฏ', '๐œตฐ',
'๐œบ ', '๐œตฑ', '๐œตฒ', '๐œตณ', '๐œตด', '๐œตต', '๐œตถ', '๐œตท', '๐œตธ', '๐œตน', '๐œตบ', '๐œตป', '๐œตผ', '๐œตฝ', '๐œตพ', '๐œตฟ',
'๐œถ€', '๐œถ', '๐œถ‚', '๐œถƒ', '๐œถ„', '๐œถ…', '๐œถ†', '๐œถ‡', '๐œถˆ', '๐œถ‰', '๐œถŠ', '๐œถ‹', '๐œถŒ', '๐œถ', '๐œถŽ', '๐œถ',
'โ–—', '๐œถ', '๐œถ‘', '๐œถ’', '๐œถ“', 'โ–š', '๐œถ”', '๐œถ•', '๐œถ–', '๐œถ—', 'โ–', '๐œถ˜', '๐œถ™', '๐œถš', '๐œถ›', 'โ–œ',
'๐œถœ', '๐œถ', '๐œถž', '๐œถŸ', '๐œถ ', '๐œถก', '๐œถข', '๐œถฃ', '๐œถค', '๐œถฅ', '๐œถฆ', '๐œถง', '๐œถจ', '๐œถฉ', '๐œถช', '๐œถซ',
'โ–‚', '๐œถฌ', '๐œถญ', '๐œถฎ', '๐œถฏ', '๐œถฐ', '๐œถฑ', '๐œถฒ', '๐œถณ', '๐œถด', '๐œถต', '๐œถถ', '๐œถท', '๐œถธ', '๐œถน', '๐œถบ',
'๐œถป', '๐œถผ', '๐œถฝ', '๐œถพ', '๐œถฟ', '๐œท€', '๐œท', '๐œท‚', '๐œทƒ', '๐œท„', '๐œท…', '๐œท†', '๐œท‡', '๐œทˆ', '๐œท‰', '๐œทŠ',
'๐œท‹', '๐œทŒ', '๐œท', '๐œทŽ', '๐œท', '๐œท', '๐œท‘', '๐œท’', '๐œท“', '๐œท”', '๐œท•', '๐œท–', '๐œท—', '๐œท˜', '๐œท™', '๐œทš',
'โ–„', '๐œท›', '๐œทœ', '๐œท', '๐œทž', 'โ–™', '๐œทŸ', '๐œท ', '๐œทก', '๐œทข', 'โ–Ÿ', '๐œทฃ', 'โ–†', '๐œทค', '๐œทฅ', 'โ–ˆ'
];

getBlockChar(pixels) {
const vals = [];
for (const { r, g, b } of pixels) {
vals.push((r + g + b >= (3 * 0xc0)) ? 1 : 0);
}

return {
char: Frame.BLOCKS_4x2[Number.parseInt(vals.reverse().join(''), 2)],
foreground: { r: 0xff, g: 0xff, b: 0xff },
background: { r: 0x00, g: 0x00, b: 0x00 }
};
}
}

const main = async () => {
const cols = 320, rows = 200;

const image = await sharp('sammy.png')
.resize(cols, rows, { fit: 'inside' })
.toColorspace('srgb');
//await image.toFile('delme.png');
const buffer = await image.raw().toBuffer({ resolveWithObject: true });

const frame = new Frame(buffer.info.width, buffer.info.height, buffer.data);

hideCursor();
saveCursor();
alternateBuffer();
clearDisplay();

frame.display();

await keypress();

normalBuffer();
restoreCursor();
showCursor();
};

await main();

0 comments on commit 290acba

Please sign in to comment.