UNPKG

@thi.ng/text-canvas

Version:

Text based canvas, drawing, plotting, tables with arbitrary formatting (incl. ANSI/HTML)

443 lines (442 loc) 12 kB
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_BLUE]: { [FG_BLUE]: FG_LIGHT_BLUE, [FG_GREEN]: FG_CYAN, [FG_RED]: FG_MAGENTA, [FG_CYAN]: FG_LIGHT_CYAN, [FG_MAGENTA]: FG_LIGHT_MAGENTA, [FG_YELLOW]: FG_LIGHT_GRAY, [FG_LIGHT_BLUE]: FG_LIGHT_CYAN, [FG_LIGHT_GREEN]: FG_LIGHT_CYAN, [FG_LIGHT_RED]: FG_LIGHT_MAGENTA, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_WHITE }, [FG_GREEN]: { [FG_GREEN]: FG_LIGHT_GREEN, [FG_RED]: FG_YELLOW, [FG_CYAN]: FG_LIGHT_CYAN, [FG_MAGENTA]: FG_LIGHT_GRAY, [FG_YELLOW]: FG_LIGHT_YELLOW, [FG_LIGHT_BLUE]: FG_LIGHT_CYAN, [FG_LIGHT_GREEN]: FG_LIGHT_GRAY, [FG_LIGHT_RED]: FG_LIGHT_YELLOW, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_WHITE }, [FG_RED]: { [FG_RED]: FG_LIGHT_RED, [FG_CYAN]: FG_LIGHT_GRAY, [FG_MAGENTA]: FG_LIGHT_MAGENTA, [FG_YELLOW]: FG_LIGHT_YELLOW, [FG_LIGHT_BLUE]: FG_LIGHT_MAGENTA, [FG_LIGHT_GREEN]: FG_LIGHT_YELLOW, [FG_LIGHT_RED]: FG_LIGHT_GRAY, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_WHITE }, // secondary [FG_CYAN]: { [FG_CYAN]: FG_LIGHT_CYAN, [FG_MAGENTA]: FG_LIGHT_GRAY, [FG_YELLOW]: FG_LIGHT_GRAY, [FG_LIGHT_BLUE]: FG_LIGHT_CYAN, [FG_LIGHT_GREEN]: FG_LIGHT_CYAN, [FG_LIGHT_RED]: FG_LIGHT_GRAY, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_WHITE }, [FG_MAGENTA]: { [FG_MAGENTA]: FG_LIGHT_MAGENTA, [FG_YELLOW]: FG_LIGHT_GRAY, [FG_LIGHT_BLUE]: FG_LIGHT_MAGENTA, [FG_LIGHT_GREEN]: FG_LIGHT_GRAY, [FG_LIGHT_RED]: FG_LIGHT_GRAY, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_WHITE }, [FG_YELLOW]: { [FG_YELLOW]: FG_LIGHT_YELLOW, [FG_LIGHT_BLUE]: FG_LIGHT_GRAY, [FG_LIGHT_GREEN]: FG_LIGHT_GRAY, [FG_LIGHT_RED]: FG_LIGHT_GRAY, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_WHITE }, // light primary [FG_LIGHT_BLUE]: { [FG_LIGHT_BLUE]: FG_LIGHT_GRAY }, [FG_LIGHT_GREEN]: { [FG_LIGHT_GREEN]: FG_LIGHT_GRAY }, [FG_LIGHT_RED]: { [FG_LIGHT_RED]: FG_LIGHT_GRAY }, // light secondary [FG_LIGHT_CYAN]: { [FG_LIGHT_CYAN]: FG_LIGHT_GRAY }, [FG_LIGHT_MAGENTA]: { [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY }, [FG_LIGHT_YELLOW]: { [FG_LIGHT_YELLOW]: FG_WHITE }, // grays [FG_GRAY]: { [FG_BLUE]: FG_LIGHT_BLUE, [FG_GREEN]: FG_LIGHT_GREEN, [FG_RED]: FG_LIGHT_RED, [FG_CYAN]: FG_LIGHT_CYAN, [FG_MAGENTA]: FG_LIGHT_MAGENTA, [FG_YELLOW]: FG_LIGHT_YELLOW, [FG_GRAY]: FG_LIGHT_GRAY, [FG_LIGHT_BLUE]: FG_LIGHT_GRAY, [FG_LIGHT_GREEN]: FG_LIGHT_GRAY, [FG_LIGHT_RED]: FG_LIGHT_GRAY, [FG_LIGHT_CYAN]: FG_LIGHT_GRAY, [FG_LIGHT_MAGENTA]: FG_LIGHT_GRAY, [FG_LIGHT_YELLOW]: FG_LIGHT_GRAY }, [FG_LIGHT_GRAY]: { [FG_BLUE]: FG_WHITE, [FG_GREEN]: FG_WHITE, [FG_RED]: FG_WHITE, [FG_CYAN]: FG_WHITE, [FG_MAGENTA]: FG_WHITE, [FG_YELLOW]: FG_WHITE, [FG_GRAY]: FG_WHITE, [FG_LIGHT_BLUE]: FG_WHITE, [FG_LIGHT_GREEN]: FG_WHITE, [FG_LIGHT_RED]: FG_WHITE, [FG_LIGHT_CYAN]: FG_WHITE, [FG_LIGHT_MAGENTA]: FG_WHITE, [FG_LIGHT_YELLOW]: FG_WHITE, [FG_LIGHT_GRAY]: FG_WHITE }, [FG_WHITE]: { [FG_BLUE]: FG_WHITE, [FG_CYAN]: FG_WHITE, [FG_GRAY]: FG_WHITE, [FG_GREEN]: FG_WHITE, [FG_RED]: FG_WHITE, [FG_MAGENTA]: FG_WHITE, [FG_YELLOW]: FG_WHITE, [FG_LIGHT_BLUE]: FG_WHITE, [FG_LIGHT_CYAN]: FG_WHITE, [FG_LIGHT_GRAY]: FG_WHITE, [FG_LIGHT_GREEN]: FG_WHITE, [FG_LIGHT_MAGENTA]: FG_WHITE, [FG_LIGHT_RED]: FG_WHITE, [FG_LIGHT_YELLOW]: 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 };