Skip to content

Commit

Permalink
feat(text-canvas): add FMT_ANSI565, update StringFormat
Browse files Browse the repository at this point in the history
- add `FMT_ANSI565` (16bit RGB565) string formatter and supporting functions
- merge ansi.ts & html.ts => format.ts
- add `StringFormat.zero` to indicate if a zero format ID should NOT
  be skipped during formatting
- update/fix `toString()` to consider new `zero` setting
- update `FMT_ANSI256`, `FMT_HTML565`
  • Loading branch information
postspectacular committed Mar 24, 2021
1 parent 0db78c0 commit 3bf5b47
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 202 deletions.
62 changes: 0 additions & 62 deletions packages/text-canvas/src/ansi.ts

This file was deleted.

11 changes: 11 additions & 0 deletions packages/text-canvas/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ export interface StringFormat {
* Suffix for each canvas row / line result string (e.g. linebreak)
*/
suffix: string;
/**
* If true, DON'T skip 0-valued format IDs during formatting.
*
* @remarks
* This is needed for various custom color-only formats, e.g. in order to
* reproduce black in `FMT_ANSI565`. In the default format, a zero refers to
* the default format of the target.
*
* @defaultValue false
*/
zero?: boolean;
}

export interface HtmlFormatOpts {
Expand Down
246 changes: 246 additions & 0 deletions packages/text-canvas/src/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { memoize1 } from "@thi.ng/memoize";
import { U8 } from "@thi.ng/strings";
import type { HtmlFormatOpts, StringFormat } from "./api";
import { defFormat } from "./string";

const ANSI_RESET = `\x1b[0m`;

const ANSI_FLAGS = ["", "1", "2", "1;2", "4", "1;4", "2;4", "1;2;4"];

/**
* String format preset, translating canvas format info to ANSI 4bit
* control sequences.
*
* https://stackoverflow.com/a/33206814/294515
*/
export const FMT_ANSI16: StringFormat = {
start: memoize1((x: number) => {
let res = [];
let y = x & 0xf;
y && res.push(29 + ((x >> 4) & 1) * 60 + y);
y = (x >> 5) & 0xf;
y && res.push(39 + ((x >> 9) & 1) * 60 + y);
y = x >> 10;
y && res.push(ANSI_FLAGS[y]);
return "\x1b[" + res.join(";") + "m";
}),
end: ANSI_RESET,
prefix: ANSI_RESET,
suffix: "\n",
};

/**
* String format preset, translating canvas format info to ANSI 8bit control
* sequences. Only foreground/background colors are supported, no other
* formatting (e.g. bold etc.).
*
* @remarks
* https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
*
* Also see {@link format256}.
*/
export const FMT_ANSI256: StringFormat = {
start: (x: number) => `\x1b[38;5;${x & 0xff};48;5;${x >>> 8}m`,
end: ANSI_RESET,
prefix: ANSI_RESET,
suffix: "\n",
zero: true,
};

const F5 = 255 / 31;
const F6 = 255 / 63;

/**
* String format preset, interpreting the canvas 16bit format info as text color
* and translating it into ANSI 24bit control sequences. No other formatting
* (e.g. bold etc.) supported in this mode.
*
* @remarks
* This mode is intended for image displays, e.g. using thi.ng/pixel and
* {@link imageRaw}. Also see {@link format556} and {@link format565_8bit}.
*
* https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
*/
export const FMT_ANSI565: StringFormat = {
start: (x: number) =>
`\x1b[38;2;${(((x >> 11) & 31) * F5) | 0};${
(((x >> 5) & 63) * F6) | 0
};${((x & 31) * F5) | 0}m`,
end: ANSI_RESET,
prefix: ANSI_RESET,
suffix: "\n",
zero: true,
};

/**
* Takes 2 ANSI256 values and returns a combined 16bit format ID.
*
* @remarks
* https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
*
* @param fg
* @param bg
*/
export const format256 = (fg: number, bg = 0) =>
((bg & 0xff) << 8) | (fg & 0xff);

/**
* Takes _normalized_ RGB values ([0..1] range) and converts them into an RGB565
* value for later use with {@link FMT_ANSI565} or {@link FMT_HTML_565}. Does
* NOT perform clipping!
*
* @param r
* @param g
* @param b
*/
export const format565 = (r: number, g: number, b: number) =>
((r * 0x1f) << 11) | ((g * 0x3f) << 5) | (b * 0x1f);

/**
* Converts RGB (8bit channels) into a 16bit RGB565 value for later use with
* {@link FMT_ANSI565} or {@link FMT_HTML_565}. Does NOT perform clipping!
*
* @param r
* @param g
* @param b
*/
export const format565_8bit = (r: number, g: number, b: number) =>
((r << 8) & 0xf800) | ((g << 3) & 0x7e0) | (b >> 3);

/**
* Syntax sugar for `defFormat(FMT_ANSI16, ...)`
*
* @param col
*/
export const defAnsi16 = (col: number) => defFormat(FMT_ANSI16, col);

/**
* Syntax sugar for `defFormat(FMT_ANSI256, ...)`
*
* @param col
*/
export const defAnsi256 = (col: number) => defFormat(FMT_ANSI256, col);

/**
* Syntax sugar for `defFormat(FMT_ANSI565, ...)`
*
* @param col
*/
export const defAnsi565 = (col: number) => defFormat(FMT_ANSI565, col);

/**
* Constructs an HTML formatter using given config options and for use w/
* default format IDs only.
*
* @param opts
*/
export const formatHtml = ({
colors,
attrib,
delim,
fg,
bg,
bold,
dim,
underline,
}: HtmlFormatOpts): StringFormat => ({
start: memoize1((x: number) => {
let y = x & 0xf;
let res = `<span ${attrib}="${fg}${
colors[(y - 1) | ((x >> 1) & 0x8)]
}${delim}`;
y = (x >> 5) & 0x0f;
y && (res += `${bg}${colors[(y - 1) | ((x >> 6) & 0x8)]}${delim}`);
x & 0x400 && (res += bold + delim);
x & 0x800 && (res += dim + delim);
x & 0x1000 && (res += underline + delim);
return res + '">';
}),
end: "</span>",
prefix: "",
suffix: "<br/>",
});

/**
* Preset HTML string formatter for use w/ default format IDs, generating
* `<span>` elements with inline CSS.
*/
export const FMT_HTML_INLINE_CSS = formatHtml({
colors: [
"#000",
"#a00",
"#0a0",
"#aa0",
"#00a",
"#a0a",
"#0aa",
"#aaa",
"#555",
"#f55",
"#5f5",
"#ff5",
"#55f",
"#f5f",
"#5ff",
"#fff",
],
attrib: "style",
delim: ";",
fg: "color:",
bg: "background:",
bold: "font-weight:bold",
dim: "opacity:0.5",
underline: "text-decoration:underline",
});

/**
* Preset HTML formatter for use w/ default format IDs, generating `<span>`
* elements with Tachyons CSS classes.
*/
export const FMT_HTML_TACHYONS = formatHtml({
colors: [
"black",
"dark-red",
"dark-green",
"gold",
"dark-blue",
"dark-pink",
"light-blue",
"moon-gray",
"mid-gray",
"red",
"green",
"yellow",
"blue",
"hot-pink",
"lightest-blue",
"white",
],
attrib: "class",
delim: " ",
fg: "",
bg: "bg-",
bold: "b",
dim: "o-50",
underline: "underline",
});

/**
* Higher order custom HTML formatter for 16bit (RGB565) colors (without
* additional formatting flags). By default assigns text color, but can be
* assigned to any CSS property via given `prop` argument.
*
* @param prop
*/
export const FMT_HTML565 = (prop = "color"): StringFormat => ({
start: memoize1(
(x) =>
`<span style="${prop}:#${U8((x >> 11) * F5)}${U8(
((x >> 5) & 0x3f) * F6
)}${U8((x & 0x1f) * F5)}">`
),
end: "</span>",
prefix: "",
suffix: "<br/>",
zero: true,
});
Loading

0 comments on commit 3bf5b47

Please sign in to comment.