This project is part of the @thi.ng/umbrella monorepo.
- About
- Installation
- Dependencies
- Usage examples
- API
- Authors
- License
Text based canvas, drawing, tables with arbitrary formatting (incl. ANSI/HTML).
STABLE - used in production
Search or submit any issues for this package
- @thi.ng/text-format - Customizable color text formatting with presets for ANSI & HTML
yarn add @thi.ng/text-canvas
ES module import:
<script type="module" src="https://cdn.skypack.dev/@thi.ng/text-canvas"></script>
For Node.js REPL:
# with flag only for < v16
node --experimental-repl-await
> const textCanvas = await import("@thi.ng/text-canvas");
Package sizes (gzipped, pre-treeshake): ESM: 5.00 KB
- @thi.ng/api
- @thi.ng/arrays
- @thi.ng/checks
- @thi.ng/geom-clip-line
- @thi.ng/math
- @thi.ng/strings
- @thi.ng/text-format
- @thi.ng/transducers
Several demos in this repo's /examples directory are using this package.
A selection:
Screenshot | Description | Live demo | Source |
---|---|---|---|
3D wireframe textmode demo | Demo | Source | |
Textmode image warping w/ 16bit color output | Demo | Source |
const c = canvas(width, height, format?, style?);
The text canvas stores all characters in a Uint32Array
with the lower 16 bits
used for the UTF-16 code and the upper 16 bits for abitrary formatting data.
The package provides its own format
IDs
which are tailored for the bundled ANSI & HTML formatters, but users are free to
choose use any other system (but then will also need to implement a custom
string formatter impl).
The default format ID layout is as shown:
Most drawing functions accept an optional format
arg, but a default
format can also be set via setFormat(canvas, formatID)
.
The following built-in format IDs are only compatible with these formatters:
FMT_ANSI16
FMT_HTML_INLINE_CSS
FMT_HTML_TACHYONS
Custom formatters are discussed further below:
These color IDs MUST be prefixed with either FG_
(foreground) or BG_
(background):
BLACK
RED
GREEN
YELLOW
BLUE
MAGENTA
CYAN
GRAY
WHITE
LIGHT_GRAY
LIGHT_RED
LIGHT_GREEN
LIGHT_YELLOW
LIGHT_BLUE
LIGHT_MAGENTA
LIGHT_CYAN
BOLD
DIM
UNDERLINE
Format IDs can be combined via the binary OR operator (|
), e.g.:
setFormat(canvas, FG_BLACK | BG_LIGHT_CYAN | BOLD | UNDERLINE);
Canvas-to-string conversion is completely customizable via the StringFormat
interface
and the following presets are supplied:
FMT_ANSI16
- translate built-in format IDs to 4bit ANSI escape sequencesFMT_ANSI256
- uses all 16 format bits for fg & bg colors (ANSI esc sequences)FMT_ANSI_RAW
- verbatim use of format IDs to ANSI sequencesFMT_HTML_INLINE_CSS
- HTML<span>
elements with inline CSSFMT_HTML_TACHYONS
- HTML<span>
elements with Tachyons CSS class namesFMT_NONE
- dummy formatter outputting plain text only (all format information discarded)
// Terminal
console.log(toString(canvas, FMT_ANSI16));
// or
console.log(toString(canvas, FMT_ANSI256));
// Browser
const el = document.createElement("pre");
el.innerHTML = toString(canvas, FMT_HTML_TACHYONS);
If targeting this output format, all 16 bits available for formatting
information are used to encode 2x 8bit foreground/background colors. Therefore,
none of the above mentioned preset color names and/or any additional formatting
flags (e.g. bold, underline etc.) cannot be used. Instead, use the
format256()
function to compute a format ID based on given FG, BG colors.
// deep purple on yellow bg
textLine(canvas, 1, 1, "hello color!", format256(19, 226));
Source: Wikipedia
Similar to the above custom ANSI format, here all available 16 bits are used to store color information, in the standard RGB565 format (5bits red, 6bits green, 5bits blue). This also means, only either the text or background color(1) can be controlled and no other formatting flag (bold, underline etc.) are available.
(1) In the ANSI version it's always only the text color.
const el = document.createElement("pre");
// format and assign text colors
el.innerHTML = toString(canvas, FMT_HTML565());
// assign bg colors
el.innerHTML = toString(canvas, FMT_HTML565("background"));
These formats are primarily intended for image display, see section below for examples...
String formatters can also be used in an ad-hoc manner, without requiring any of the other text canvas functionality.
// create & use a HTML formatter
defFormat(FMT_HTML_INLINE_CSS, FG_LIGHT_RED | BG_GRAY)("hello")
// "<span style="color:#f55;background:#555;">hello</span>"
// create & use an ANSI formatter
defFormat(FMT_ANSI16, FG_LIGHT_RED | BG_GRAY)("hello")
// "\x1B[91;100mhello\x1B[0m"
// ANSI syntax sugar (same result as above)
defAnsi16(FG_LIGHT_RED | BG_GRAY)("hello")
// "\x1B[91;100mhello\x1B[0m"
Furthermore, defFormatPresets()
can be used to create formatting functions for
all 16 preset foreground color IDs for a given string format
strategy:
const ansi = defFormatPresets(FMT_ANSI16);
`${ansi.green("hello")} ${ansi.lightRed("world")}!`;
// '\x1B[32mhello\x1B[0m \x1B[91mworld\x1B[0m!'
const html = defFormatPresets(FMT_HTML_TACHYONS);
`${html.green("hello")} ${html.lightRed("world")}!`;
// '<span class="dark-green ">hello</span> <span class="red ">world</span>!'
Built-in style presets:
STYLE_ASCII
STYLE_THIN
STYLE_THIN_ROUNDED
STYLE_DASHED
STYLE_DASHED_ROUNDED
STYLE_DOUBLE
Functions:
beginStyle(canvas, style)
endStyle(canvas)
All drawing operations are constrained to the currently active clipping rect (by default full canvas). The canvas maintains a stack of such clipping regions, each newly pushed one being intersected with the previous top-of-stack rect:
beginClip(canvas, x, y, w, h)
- push new clip rectendClip(canvas)
- restore previous clip rect
┌──────────────────┐
│ A │
│ ╔════════╗─────────┐
│ ║ ║ │
│ ║ A & B ║ │
│ ║ ║ │
└─────────╚════════╝ │
│ B │
└──────────────────┘
-
line
-
hline
-
vline
-
circle
-
clear
-
fillRect
-
strokeRect
blit
resize
extract
scrollV
image
/imageRaw
/imageCanvas565
/imageString565
imageBraille
/imageCanvasBraille
/imageStringBraille
import { RGB565 } from "@thi.ng/pixel";
import { read } from "@thi.ng/pixel-io-netpbm";
// resize non-proportionally (to compensate
// for character aspect ratio, YMMV)
const img = read(readFileSync("chroma-rings.ppm"))
.resize(32, 32 / 2.25)
.as(RGB565)
// requires an ANSI 24bit compatible terminal
console.log(imageString565(img));
textLine
textLines
textColumn
(word wrapped)textBox
(word wrapped)
The following are string builders only, draw result via text functions:
barHorizontal
barVertical
barChartHStr
barChartVStr
Tables support individual column width, automatic (or user defined) row heights, cell padding, as well as global and per-cell formats and the following border style options:
Border style | Result |
---|---|
Border.ALL |
|
Border.NONE |
|
Border.H |
|
Border.V |
|
Border.FRAME |
|
Border.FRAME_H |
|
Border.FRAME_V |
Table cell contents will be word-wrapped. By default, individual words longer
than the configured cell width will be truncated, but can be forced to wrap by
enabling the hard
option (see example below).
import { repeatedly } from "@thi.ng/transducers";
import * as tc from "@thi.ng/text-canvas";
// generate 20 random values
const data = repeatedly(() => Math.random(), 20)
// format as bar chart string
const chart = tc.barChartHStr(4, data, 0, 1);
// create text canvas
const canvas = new tc.Canvas(64, 20);
// create table
tc.table(
canvas,
0,
0,
{
// column defs
cols: [{ width: 4 }, { width: 20 }, { width: 8 }],
// default cell format
format: tc.FG_BLACK | tc.BG_LIGHT_CYAN,
// default format for header cells (1st row)
formatHead: tc.FG_RED | tc.BG_LIGHT_CYAN | tc.BOLD | tc.UNDERLINE,
// border line style
style: tc.STYLE_DASHED_ROUNDED,
// border mode
border: tc.Border.ALL,
// internal cell padding [h,v]
padding: [1, 0],
// hard word wrap
hard: true,
},
// table contents (row major)
// each cell either a string or RawCell object
[
["ID", "Main", "Comment"],
[
"0001",
{ body: chart, format: tc.FG_BLUE | tc.BG_LIGHT_CYAN },
"This is a test!"
],
["0002", "Random data plot", "Word wrapped content"],
["0003", { body: "More details...", height: 4 }, ""]
]
);
// output as ANSI formatted string
console.log(tc.toString(canvas, tc.FMT_ANSI16));
For even more detailed control, tables can also be pre-initialized prior
to creation of the canvas via
initTable()
and then drawn via
drawTable()
.
The initTable
function returns an object also containing the computed
table size (width
, height
keys) which can then be used to create a
canvas with the required size...
For convenience, the tableCanvas()
function can be used to combine
these steps and to create an auto-sized canvas with the rendered table
as content.
┌───┐
┌──────────────────────┐
│ @thi.ng/text-canvas │
│ wireframe cube │++++++++++
│ │ +++++++++++ ┌───┐
│ x: 0.42 │ ++++│ 6 │
│ y: 0.30 │ ┌───┐ ++++++++ └───┘
└──────────────────────┘++++++++│ 7 │+ +
+ └───┘ └───┘ +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + ┌───┐ +
+ + +│ 3 │ +
+ ┌───┐+++ └───┘ +
+ │ 0 │ + +
+ └───┘ + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + ┌───┐
+ + │ 2 │
+ + ++└───┘
+ + +++
+ + ++
+ + +++
++ ++
Code for this above example output (CLI version):
import * as geom from "@thi.ng/geom";
import * as mat from "@thi.ng/matrices";
import * as tc from "@thi.ng/text-canvas";
const W = 64;
const H = 32;
// create text canvas
const canvas = new tc.Canvas(W, H, tc.BG_BLACK, tc.STYLE_THIN);
// cube corner vertices
const cube = geom.vertices(geom.center(geom.aabb(1))!);
// edge list (vertex indices)
const edges = [
[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6],
[6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]
];
// animated parameters
let rotx = 0;
let roty = 0;
// 3D transformation matrices
const view = mat.lookAt([], [0, 0, 1], [0, 0, 0], [0, 1, 0]);
const proj = mat.perspective([], 90, W / H, 0.1, 10);
const viewp = mat.viewport([], 0, W, H, 0);
setInterval(() => {
tc.clear(canvas, true);
// model rotation matrix
const model = mat.concat(
[],
mat.rotationX44([], rotx += 0.01),
mat.rotationY44([], roty += 0.03)
);
// combined model-view-projection matrix
const mvp = mat.concat([], proj, view, model);
// draw cube instances
// project 3D points to 2D viewport (canvas coords)
const pts = cube.map((p) => mat.project3([], mvp, viewp, p)!);
// draw cube edges
for (let e of edges) {
const a = pts[e[0]];
const b = pts[e[1]];
tc.line(canvas, a[0], a[1], b[0], b[1], "+", tc.FG_WHITE | tc.BG_RED);
}
// draw vertex labels
canvas.format = tc.FG_WHITE | tc.BG_BLUE;
for (let i = 0; i < 8; i++) {
const p = pts[i];
tc.textBox(canvas, p[0] - 1, p[1] - 1, 5, 3, ` ${i} `);
}
tc.textBox(
canvas,
2, 1, 24, -1,
`@thi.ng/text-canvas wireframe cube\n\nx: ${rotx.toFixed(2)}\ny: ${roty.toFixed(2)}`,
{
format: tc.FG_BLACK | tc.BG_LIGHT_CYAN,
padding: [1, 0]
}
);
// draw canvas
console.clear();
// output as ANSI formatted string
console.log(tc.toString(canvas, tc.FMT_ANSI16));
// output as plain text
// console.log(tc.toString(canvas));
}, 15);
Karsten Schmidt
If this project contributes to an academic publication, please cite it as:
@misc{thing-text-canvas,
title = "@thi.ng/text-canvas",
author = "Karsten Schmidt",
note = "https://thi.ng/text-canvas",
year = 2020
}
© 2020 - 2021 Karsten Schmidt // Apache Software License 2.0