@thi.ng/text-canvas
Version:
Text based canvas, drawing, plotting, tables with arbitrary formatting (incl. ANSI/HTML)
443 lines (442 loc) • 12 kB
JavaScript
import { blit1d, blitPred1d } from "@thi.ng/arrays/blit";
import { peek } from "@thi.ng/arrays/peek";
import { isNumber } from "@thi.ng/checks/is-number";
import { clamp0 } from "@thi.ng/math/interval";
import {
FG_BLUE,
FG_CYAN,
FG_GRAY,
FG_GREEN,
FG_LIGHT_BLUE,
FG_LIGHT_CYAN,
FG_LIGHT_GRAY,
FG_LIGHT_GREEN,
FG_LIGHT_MAGENTA,
FG_LIGHT_RED,
FG_LIGHT_YELLOW,
FG_MAGENTA,
FG_RED,
FG_YELLOW,
FG_WHITE
} from "@thi.ng/text-format";
import { FMT_ANSI565 } from "@thi.ng/text-format/ansi";
import {
SHADES_BLOCK
} from "./api.js";
import { Canvas, canvas } from "./canvas.js";
import { formatCanvas } from "./format.js";
import { charCode, intersectRect } from "./utils.js";
const __initBlit = (dest, x, y, src) => {
x |= 0;
y |= 0;
const { data: sbuf, width: sw, height: sh } = src;
const { data: dbuf, width: dw } = dest;
const {
x1,
y1,
y2,
w: iw,
h: ih
} = intersectRect(
{ x1: x, y1: y, x2: x + sw, y2: y + sh, w: sw, h: sh },
peek(dest.clipRects)
);
if (!iw || !ih) return;
const sx = clamp0(x1 - x);
const sy = clamp0(y1 - y);
return { sbuf, dbuf, sw, dw, x, y, x1, y1, y2, iw, ih, sx, sy };
};
const blit = (dest, src, x = 0, y = 0) => {
const state = __initBlit(dest, x, y, src);
if (!state) return;
const { sbuf, dbuf, x1, y1, y2, sx, sy, iw, sw, dw } = state;
for (let yy = sy, dy = y1; dy < y2; yy++, dy++) {
let sidx = sx + yy * sw;
let didx = x1 + dy * dw;
dbuf.set(sbuf.subarray(sidx, sidx + iw), didx);
}
};
const blitMask = (dest, src, x = 0, y = 0, mask = 32) => {
const state = __initBlit(dest, x, y, src);
if (!state) return;
const { sbuf, dbuf, x1, y1, y2, sx, sy, iw, sw, dw } = state;
mask = charCode(mask, 0);
for (let yy = sy, dy = y1; dy < y2; yy++, dy++) {
let sidx = sx + yy * sw;
let didx = x1 + dy * dw;
blit1d(dbuf, didx, sbuf.subarray(sidx, sidx + iw), mask);
}
};
const blitBarsV = (dest, src, x = 0, y = 0, blend = blendBarsVAdd) => {
const state = __initBlit(dest, x, y, src);
if (!state) return;
const { sbuf, dbuf, x1, y1, y2, sx, sy, iw, sw, dw } = state;
for (let yy = sy, dy = y1; dy < y2; yy++, dy++) {
let sidx = sx + yy * sw;
let didx = x1 + dy * dw;
blitPred1d(dbuf, didx, sbuf.subarray(sidx, sidx + iw), (a, b, x2) => {
const ac = a & 65535;
return ac === 32 ? void 0 : ac > 9600 && ac < 9609 ? blend(a, b, x2, yy) : a;
});
}
};
const blendBarsVAdd = (a, b) => {
const ac = a & 65535;
const fmtA = a >> 16 & 31;
const fgA = fmtA & 31;
const bc = b & 65535;
const fmtB = b >> 16;
const bgB = fmtB >> 5;
const fgB = fmtB & 31;
let col;
let col2;
if (bc === 32) {
col = __blend(fgA, bgB) || fgA;
return ac == 9608 ? col << 21 | 32 : bgB << 21 | col << 16 | ac;
}
if (ac <= bc) {
col2 = bc > 9604 ? fgB : bgB;
col = __blend(fgA, col2);
return col2 << 21 | col << 16 | ac + bc >> 1;
} else {
col = __blend(fgA, fgB) || fgA;
col2 = __blend(fgA, bgB) || fgA;
return col2 << 21 | col << 16 | bc;
}
};
const __blend = (a, b) => BLEND_ADD[b]?.[a] || BLEND_ADD[a]?.[b] || 0;
const BLEND_ADD = {
// primary
[ ]: {
[ ]: FG_LIGHT_BLUE,
[ ]: FG_CYAN,
[ ]: FG_MAGENTA,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_WHITE
},
[ ]: {
[ ]: FG_LIGHT_GREEN,
[ ]: FG_YELLOW,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_YELLOW,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_YELLOW,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_WHITE
},
[ ]: {
[ ]: FG_LIGHT_RED,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_YELLOW,
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_YELLOW,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_WHITE
},
// secondary
[ ]: {
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_WHITE
},
[ ]: {
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_WHITE
},
[ ]: {
[ ]: FG_LIGHT_YELLOW,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_WHITE
},
// light primary
[ ]: {
[ ]: FG_LIGHT_GRAY
},
[ ]: {
[ ]: FG_LIGHT_GRAY
},
[ ]: {
[ ]: FG_LIGHT_GRAY
},
// light secondary
[ ]: {
[ ]: FG_LIGHT_GRAY
},
[ ]: {
[ ]: FG_LIGHT_GRAY
},
[ ]: {
[ ]: FG_WHITE
},
// grays
[ ]: {
[ ]: FG_LIGHT_BLUE,
[ ]: FG_LIGHT_GREEN,
[ ]: FG_LIGHT_RED,
[ ]: FG_LIGHT_CYAN,
[ ]: FG_LIGHT_MAGENTA,
[ ]: FG_LIGHT_YELLOW,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY,
[ ]: FG_LIGHT_GRAY
},
[ ]: {
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE
},
[ ]: {
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE,
[ ]: FG_WHITE
}
};
const resize = (canvas2, newWidth, newHeight) => {
if (canvas2.width === newWidth && canvas2.height === newHeight) return;
const dest = new Canvas(newWidth, newHeight);
dest.data.fill(charCode(32, canvas2.format));
blit(dest, canvas2);
canvas2.data = dest.data;
canvas2.size[0] = newWidth;
canvas2.size[1] = newHeight;
canvas2.clipRects = [
{
x1: 0,
y1: 0,
x2: newWidth,
y2: newHeight,
w: newWidth,
h: newHeight
}
];
};
const extract = (canvas2, x, y, w, h) => {
const dest = new Canvas(w, h, canvas2.format, peek(canvas2.styles));
blit(dest, canvas2, -x, -y);
return dest;
};
const scrollV = (canvas2, dy, clear = 32) => {
const { data, width } = canvas2;
const ch = charCode(clear, canvas2.format);
dy *= width;
if (dy < 0) {
data.copyWithin(-dy, 0, dy);
data.fill(ch, 0, -dy);
} else if (dy > 0) {
data.copyWithin(0, dy);
data.fill(ch, -dy);
}
};
const image = (canvas2, x, y, w, h, pixels, opts = {}) => {
x |= 0;
y |= 0;
w |= 0;
h |= 0;
const { data, width } = canvas2;
const {
x1,
y1,
x2,
y2,
sx,
sy,
w: iw,
h: ih
} = __imgRect(canvas2, x, y, w, h);
if (!iw || !ih) return;
const {
chars = SHADES_BLOCK,
format = canvas2.format,
gamma = 1,
invert = false,
bits = 8
} = opts;
const fmt = isNumber(format) ? () => format : format;
const max = (1 << bits) - 1;
const mask = invert ? max : 0;
const norm = 1 / max;
const num = chars.length - 1;
for (let yy = sy, dy = y1; dy < y2; yy++, dy++) {
let sidx = sx + yy * w;
let didx = x1 + dy * width;
for (let xx = sx, dx = x1; dx < x2; xx++, dx++) {
const col = Math.pow((pixels[sidx++] ^ mask) * norm, gamma);
data[didx++] = charCode(chars[col * num + 0.5 | 0], fmt(col));
}
}
};
const imageRaw = (canvas2, x, y, w, h, pixels, char = "\u2588") => {
x |= 0;
y |= 0;
w |= 0;
h |= 0;
const { data, width } = canvas2;
const {
x1,
y1,
x2,
y2,
sx,
sy,
w: iw,
h: ih
} = __imgRect(canvas2, x, y, w, h);
if (!iw || !ih) return;
const code = char.charCodeAt(0);
for (let yy = sy, dy = y1; dy < y2; yy++, dy++) {
let sidx = sx + yy * w;
let didx = x1 + dy * width;
for (let xx = sx, dx = x1; dx < x2; xx++, dx++) {
data[didx++] = code | (pixels[sidx++] & 65535) << 16;
}
}
};
const imageRawFmtOnly = (canvas2, x, y, w, h, pixels) => {
x |= 0;
y |= 0;
w |= 0;
h |= 0;
const { data, width } = canvas2;
const {
x1,
y1,
x2,
y2,
sx,
sy,
w: iw,
h: ih
} = __imgRect(canvas2, x, y, w, h);
if (!iw || !ih) return;
for (let yy = sy, dy = y1; dy < y2; yy++, dy++) {
let sidx = sx + yy * w;
let didx = x1 + dy * width;
for (let xx = sx, dx = x1; dx < x2; xx++, dx++) {
data[didx] = data[didx] & 65535 | (pixels[sidx++] & 65535) << 16;
didx++;
}
}
};
const imageBraille = (canvas2, x, y, w, h, pixels, thresh, format) => {
x |= 0;
y |= 0;
w |= 0;
h |= 0;
const { data, width } = canvas2;
const fmt = (format !== void 0 ? format : canvas2.format) << 16;
const {
x1,
y1,
x2,
y2,
sx,
sy,
w: iw,
h: ih
} = __imgRect(canvas2, x, y, w >> 1, h >> 2);
if (!iw || !ih) return;
const w2 = w * 2;
const w3 = w * 3;
const braille = (i) => (pixels[i] >= thresh ? 1 : 0) | (pixels[i + w] >= thresh ? 2 : 0) | (pixels[i + w2] >= thresh ? 4 : 0) | (pixels[i + w3] >= thresh ? 8 : 0) | (pixels[i + 1] >= thresh ? 16 : 0) | (pixels[i + w + 1] >= thresh ? 32 : 0) | (pixels[i + w2 + 1] >= thresh ? 64 : 0) | (pixels[i + w3 + 1] >= thresh ? 128 : 0) | 10240;
for (let yy = sy, dy = y1; dy < y2; yy += 4, dy++) {
let sidx = sx + yy * w;
let didx = x1 + dy * width;
for (let xx = sx, dx = x1; dx < x2; xx += 2, dx++, sidx += 2) {
data[didx++] = braille(sidx) | fmt;
}
}
};
const imageCanvasBraille = (src, thresh, format = 0) => {
const dest = canvas(src.width >> 1, src.height >> 2);
imageBraille(dest, 0, 0, src.width, src.height, src.data, thresh, format);
return dest;
};
const imageStringBraille = (src, thresh) => formatCanvas(imageCanvasBraille(src, thresh, 0));
const imageCanvas565 = (src, char) => {
const dest = canvas(src.width, src.height);
imageRaw(dest, 0, 0, src.width, src.height, src.data, char);
return dest;
};
const imageString565 = (src, char, fmt = FMT_ANSI565) => formatCanvas(imageCanvas565(src, char), fmt);
const __imgRect = (canvas2, x, y, w, h) => {
const rect = intersectRect(
{ x1: x, y1: y, x2: x + w, y2: y + h, w, h },
peek(canvas2.clipRects)
);
rect.sx = clamp0(rect.x1 - x);
rect.sy = clamp0(rect.y1 - y);
return rect;
};
export {
BLEND_ADD,
blendBarsVAdd,
blit,
blitBarsV,
blitMask,
extract,
image,
imageBraille,
imageCanvas565,
imageCanvasBraille,
imageRaw,
imageRawFmtOnly,
imageString565,
imageStringBraille,
resize,
scrollV
};