gw-canvas
Version:
Library for rendering colorized bitmap fonts. Very fast, suitable for applications where the whole canvas needs frequent redrawing.
1,301 lines (1,277 loc) • 43.5 kB
JavaScript
const VS = `
#version 300 es
in vec2 position;
in uvec2 offset;
in uint fg;
in uint bg;
in uint glyph;
out vec2 fsOffset;
out vec4 fgRgb;
out vec4 bgRgb;
flat out uvec2 fontPos;
uniform int depth;
void main() {
float fdepth = float(depth) / 255.0;
gl_Position = vec4(position, fdepth, 1.0);
float fgr = float((fg & uint(0xF000)) >> 12);
float fgg = float((fg & uint(0x0F00)) >> 8);
float fgb = float((fg & uint(0x00F0)) >> 4);
float fga = float((fg & uint(0x000F)) >> 0);
fgRgb = vec4(fgr, fgg, fgb, fga) / 15.0;
float bgr = float((bg & uint(0xF000)) >> 12);
float bgg = float((bg & uint(0x0F00)) >> 8);
float bgb = float((bg & uint(0x00F0)) >> 4);
float bga = float((bg & uint(0x000F)) >> 0);
bgRgb = vec4(bgr, bgg, bgb, bga) / 15.0;
uint glyphX = (glyph & uint(0xF));
uint glyphY = (glyph >> 4);
fontPos = uvec2(glyphX, glyphY);
fsOffset = vec2(offset);
}`.trim();
const FS = `
#version 300 es
precision highp float;
in vec2 fsOffset;
in vec4 fgRgb;
in vec4 bgRgb;
flat in uvec2 fontPos;
out vec4 fragColor;
uniform sampler2D font;
uniform uvec2 tileSize;
void main() {
uvec2 fontPx = (tileSize * fontPos) + uvec2(vec2(tileSize) * fsOffset);
vec4 texel = texelFetch(font, ivec2(fontPx), 0).rgba;
fragColor = vec4(mix(bgRgb.rgb, fgRgb.rgb, texel.rgb), mix(bgRgb.a, fgRgb.a, texel.r));
}`.trim();
class Glyphs {
constructor(opts = {}) {
this._tileWidth = 12;
this._tileHeight = 16;
this.needsUpdate = true;
this._map = {};
opts.font = opts.font || 'monospace';
this._node = document.createElement('canvas');
this._ctx = this.node.getContext('2d');
this._configure(opts);
}
static fromImage(src) {
if (typeof src === 'string') {
if (src.startsWith('data:'))
throw new Error('Glyph: You must load a data string into an image element and use that.');
const el = document.getElementById(src);
if (!el)
throw new Error('Glyph: Failed to find image element with id:' + src);
src = el;
}
const glyph = new this({ tileWidth: src.width / 16, tileHeight: src.height / 16 });
glyph._ctx.drawImage(src, 0, 0);
return glyph;
}
static fromFont(src) {
if (typeof src === 'string') {
src = { font: src };
}
const glyphs = new this(src);
const basicOnly = src.basicOnly || src.basic || false;
glyphs._initGlyphs(basicOnly);
return glyphs;
}
get node() { return this._node; }
get ctx() { return this._ctx; }
get tileWidth() { return this._tileWidth; }
get tileHeight() { return this._tileHeight; }
get pxWidth() { return this._node.width; }
get pxHeight() { return this._node.height; }
forChar(ch) {
if (ch === null || ch === undefined)
return -1;
return this._map[ch] || -1;
}
_configure(opts) {
this._tileWidth = opts.tileWidth || this.tileWidth;
this._tileHeight = opts.tileHeight || this.tileHeight;
this.node.width = 16 * this.tileWidth;
this.node.height = 16 * this.tileHeight;
this._ctx.fillStyle = 'black';
this._ctx.fillRect(0, 0, this.pxWidth, this.pxHeight);
const size = opts.fontSize || opts.size || Math.max(this.tileWidth, this.tileHeight);
this._ctx.font = '' + size + 'px ' + opts.font;
this._ctx.textAlign = 'center';
this._ctx.textBaseline = 'middle';
this._ctx.fillStyle = 'white';
}
draw(n, ch) {
if (n > 256)
throw new Error('Cannot draw more than 256 glyphs.');
const x = (n % 16) * this.tileWidth;
const y = Math.floor(n / 16) * this.tileHeight;
const cx = x + Math.floor(this.tileWidth / 2);
const cy = y + Math.floor(this.tileHeight / 2);
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(x, y, this.tileWidth, this.tileHeight);
this._ctx.clip();
if (typeof ch === 'function') {
ch(this._ctx, x, y, this.tileWidth, this.tileHeight);
}
else {
if (this._map[ch] === undefined)
this._map[ch] = n;
this._ctx.fillText(ch, cx, cy);
}
this._ctx.restore();
this.needsUpdate = true;
}
_initGlyphs(basicOnly = false) {
for (let i = 32; i < 127; ++i) {
this.draw(i, String.fromCharCode(i));
}
if (!basicOnly) {
[' ', '\u263a', '\u263b', '\u2665', '\u2666', '\u2663', '\u2660', '\u263c',
'\u2600', '\u2605', '\u2606', '\u2642', '\u2640', '\u266a', '\u266b', '\u2638',
'\u25b6', '\u25c0', '\u2195', '\u203c', '\u204b', '\u262f', '\u2318', '\u2616',
'\u2191', '\u2193', '\u2192', '\u2190', '\u2126', '\u2194', '\u25b2', '\u25bc',
].forEach((ch, i) => {
this.draw(i, ch);
});
// [
// '\u2302',
// '\u2b09', '\u272a', '\u2718', '\u2610', '\u2611', '\u25ef', '\u25ce', '\u2690',
// '\u2691', '\u2598', '\u2596', '\u259d', '\u2597', '\u2744', '\u272d', '\u2727',
// '\u25e3', '\u25e4', '\u25e2', '\u25e5', '\u25a8', '\u25a7', '\u259a', '\u265f',
// '\u265c', '\u265e', '\u265d', '\u265b', '\u265a', '\u301c', '\u2694', '\u2692',
// '\u25b6', '\u25bc', '\u25c0', '\u25b2', '\u25a4', '\u25a5', '\u25a6', '\u257a',
// '\u257b', '\u2578', '\u2579', '\u2581', '\u2594', '\u258f', '\u2595', '\u272d',
// '\u2591', '\u2592', '\u2593', '\u2503', '\u252b', '\u2561', '\u2562', '\u2556',
// '\u2555', '\u2563', '\u2551', '\u2557', '\u255d', '\u255c', '\u255b', '\u2513',
// '\u2517', '\u253b', '\u2533', '\u2523', '\u2501', '\u254b', '\u255e', '\u255f',
// '\u255a', '\u2554', '\u2569', '\u2566', '\u2560', '\u2550', '\u256c', '\u2567',
// '\u2568', '\u2564', '\u2565', '\u2559', '\u2558', '\u2552', '\u2553', '\u256b',
// '\u256a', '\u251b', '\u250f', '\u2588', '\u2585', '\u258c', '\u2590', '\u2580',
// '\u03b1', '\u03b2', '\u0393', '\u03c0', '\u03a3', '\u03c3', '\u03bc', '\u03c4',
// '\u03a6', '\u03b8', '\u03a9', '\u03b4', '\u221e', '\u03b8', '\u03b5', '\u03b7',
// '\u039e', '\u00b1', '\u2265', '\u2264', '\u2234', '\u2237', '\u00f7', '\u2248',
// '\u22c4', '\u22c5', '\u2217', '\u27b5', '\u2620', '\u2625', '\u25fc', '\u25fb'
// ].forEach( (ch, i) => {
// this.draw(i + 127, ch);
// });
['\u2302',
'\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', '\u00E0', '\u00E5', '\u00E7',
'\u00EA', '\u00EB', '\u00E8', '\u00EF', '\u00EE', '\u00EC', '\u00C4', '\u00C5',
'\u00C9', '\u00E6', '\u00C6', '\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9',
'\u00FF', '\u00D6', '\u00DC', '\u00A2', '\u00A3', '\u00A5', '\u20A7', '\u0192',
'\u00E1', '\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1', '\u00AA', '\u00BA',
'\u00BF', '\u2310', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', '\u00BB',
'\u2591', '\u2592', '\u2593', '\u2502', '\u2524', '\u2561', '\u2562', '\u2556',
'\u2555', '\u2563', '\u2551', '\u2557', '\u255D', '\u255C', '\u255B', '\u2510',
'\u2514', '\u2534', '\u252C', '\u251C', '\u2500', '\u253C', '\u255E', '\u255F',
'\u255A', '\u2554', '\u2569', '\u2566', '\u2560', '\u2550', '\u256C', '\u2567',
'\u2568', '\u2564', '\u2565', '\u2559', '\u2558', '\u2552', '\u2553', '\u256B',
'\u256A', '\u2518', '\u250C', '\u2588', '\u2584', '\u258C', '\u2590', '\u2580',
'\u03B1', '\u00DF', '\u0393', '\u03C0', '\u03A3', '\u03C3', '\u00B5', '\u03C4',
'\u03A6', '\u0398', '\u03A9', '\u03B4', '\u221E', '\u03C6', '\u03B5', '\u2229',
'\u2261', '\u00B1', '\u2265', '\u2264', '\u2320', '\u2321', '\u00F7', '\u2248',
'\u00B0', '\u2219', '\u00B7', '\u221A', '\u207F', '\u00B2', '\u25A0', '\u00A0'
].forEach((ch, i) => {
this.draw(i + 127, ch);
});
}
}
}
const colors = {};
function clamp$1(v, n, x) {
return Math.max(n, Math.min(x, v));
}
// All colors are const!!!
class Color {
// values are 0-100 for normal RGBA
constructor(r = -1, g = 0, b = 0, a = 100) {
if (r < 0) {
a = 0;
r = 0;
}
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
rgb() {
return [this.r, this.g, this.b];
}
rgba() {
return [this.r, this.g, this.b, this.a];
}
isNull() {
return this.a === 0;
}
alpha(v) {
return new Color(this.r, this.g, this.b, clamp$1(v, 0, 100));
}
// luminosity (0-100)
get l() {
return Math.round(0.5 *
(Math.min(this.r, this.g, this.b) + Math.max(this.r, this.g, this.b)));
}
// saturation (0-100)
get s() {
if (this.l >= 100)
return 0;
return Math.round(((Math.max(this.r, this.g, this.b) - Math.min(this.r, this.g, this.b)) *
(100 - Math.abs(this.l * 2 - 100))) /
100);
}
// hue (0-360)
get h() {
let H = 0;
let R = this.r;
let G = this.g;
let B = this.b;
if (R >= G && G >= B) {
H = 60 * ((G - B) / (R - B));
}
else if (G > R && R >= B) {
H = 60 * (2 - (R - B) / (G - B));
}
else if (G >= B && B > R) {
H = 60 * (2 + (B - R) / (G - R));
}
else if (B > G && G > R) {
H = 60 * (4 - (G - R) / (B - R));
}
else if (B > R && R >= G) {
H = 60 * (4 + (R - G) / (B - G));
}
else {
H = 60 * (6 - (B - G) / (R - G));
}
return Math.round(H);
}
equals(other) {
if (typeof other === "string") {
if (other.startsWith("#")) {
if (other.length == 4) {
return this.css() === other;
}
}
if (this.name && this.name === other)
return true;
}
else if (typeof other === "number") {
const O = from(other);
return this.css() === O.css();
}
const O = from(other);
if (this.isNull())
return O.isNull();
if (O.isNull())
return false;
return this.r === O.r && this.g === O.g && this.b === O.b && this.a === O.a;
}
toInt() {
// if (this.isNull()) return -1;
const r = Math.max(0, Math.min(15, Math.round((this.r / 100) * 15)));
const g = Math.max(0, Math.min(15, Math.round((this.g / 100) * 15)));
const b = Math.max(0, Math.min(15, Math.round((this.b / 100) * 15)));
const a = Math.max(0, Math.min(15, Math.round((this.a / 100) * 15)));
// TODO - alpha
return (r << 12) + (g << 8) + (b << 4) + a;
}
toLight() {
return this.rgb();
}
clamp() {
if (this.isNull())
return this;
return make([
clamp$1(this.r, 0, 100),
clamp$1(this.g, 0, 100),
clamp$1(this.b, 0, 100),
clamp$1(this.a, 0, 100),
]);
}
blend(other) {
const O = from(other);
if (O.isNull())
return this;
if (O.a === 100)
return O;
const pct = O.a / 100;
const keepPct = 1 - pct;
const newColor = make(Math.round(this.r * keepPct + O.r * pct), Math.round(this.g * keepPct + O.g * pct), Math.round(this.b * keepPct + O.b * pct), Math.round(O.a + this.a * keepPct));
return newColor;
}
mix(other, percent) {
const O = from(other);
if (O.isNull())
return this;
if (percent >= 100)
return O;
const pct = clamp$1(percent, 0, 100) / 100;
const keepPct = 1 - pct;
const newColor = make(Math.round(this.r * keepPct + O.r * pct), Math.round(this.g * keepPct + O.g * pct), Math.round(this.b * keepPct + O.b * pct), (this.isNull() ? 100 : this.a) * keepPct + O.a * pct);
return newColor;
}
// Only adjusts r,g,b
lighten(percent) {
if (this.isNull())
return this;
if (percent <= 0)
return this;
const pct = clamp$1(percent, 0, 100) / 100;
const keepPct = 1 - pct;
return make(Math.round(this.r * keepPct + 100 * pct), Math.round(this.g * keepPct + 100 * pct), Math.round(this.b * keepPct + 100 * pct), this.a);
}
// Only adjusts r,g,b
darken(percent) {
if (this.isNull())
return this;
const pct = clamp$1(percent, 0, 100) / 100;
const keepPct = 1 - pct;
return make(Math.round(this.r * keepPct + 0 * pct), Math.round(this.g * keepPct + 0 * pct), Math.round(this.b * keepPct + 0 * pct), this.a);
}
bake(_clearDancing = false) {
return this;
}
// Adds a color to this one
add(other, percent = 100) {
const O = from(other);
if (O.isNull())
return this;
const alpha = (O.a / 100) * (percent / 100);
return new Color(Math.round(this.r + O.r * alpha), Math.round(this.g + O.g * alpha), Math.round(this.b + O.b * alpha), clamp$1(Math.round(this.a + alpha * 100), 0, 100));
}
scale(percent) {
if (this.isNull() || percent == 100)
return this;
const pct = Math.max(0, percent) / 100;
return make(Math.round(this.r * pct), Math.round(this.g * pct), Math.round(this.b * pct), this.a);
}
multiply(other) {
if (this.isNull())
return this;
let data;
if (Array.isArray(other)) {
if (other.length < 3)
throw new Error("requires at least r,g,b values.");
data = other;
}
else {
if (other.isNull())
return this;
data = other.rgb();
}
const pct = (data[3] || 100) / 100;
return make(Math.round(this.r * (data[0] / 100) * pct), Math.round(this.g * (data[1] / 100) * pct), Math.round(this.b * (data[2] / 100) * pct), 100);
}
// scales rgb down to a max of 100
normalize() {
if (this.isNull())
return this;
const max = Math.max(this.r, this.g, this.b);
if (max <= 100)
return this;
return make(Math.round((100 * this.r) / max), Math.round((100 * this.g) / max), Math.round((100 * this.b) / max), 100);
}
/**
* Returns the css code for the current RGB values of the color.
* @param base256 - Show in base 256 (#abcdef) instead of base 16 (#abc)
*/
css() {
if (this.a !== 100) {
const v = this.toInt();
// if (v < 0) return "transparent";
return "#" + v.toString(16).padStart(4, "0");
}
const v = this.toInt();
// if (v < 0) return "transparent";
return "#" + v.toString(16).padStart(4, "0").substring(0, 3);
}
toString() {
if (this.name)
return this.name;
// if (this.isNull()) return "null color";
return this.css();
}
}
function fromArray(vals, base256 = false) {
while (vals.length < 3)
vals.push(0);
if (base256) {
for (let i = 0; i < 3; ++i) {
vals[i] = Math.round(((vals[i] || 0) * 100) / 255);
}
}
return new Color(...vals);
}
function fromCss(css) {
if (!css.startsWith("#")) {
throw new Error('Color CSS strings must be of form "#abc" or "#abcdef" - received: [' +
css +
"]");
}
const c = Number.parseInt(css.substring(1), 16);
let r, g, b;
if (css.length == 4) {
r = Math.round(((c >> 8) / 15) * 100);
g = Math.round((((c & 0xf0) >> 4) / 15) * 100);
b = Math.round(((c & 0xf) / 15) * 100);
}
else {
r = Math.round(((c >> 16) / 255) * 100);
g = Math.round((((c & 0xff00) >> 8) / 255) * 100);
b = Math.round(((c & 0xff) / 255) * 100);
}
return new Color(r, g, b);
}
function fromName(name) {
const c = colors[name];
if (!c) {
throw new Error("Unknown color name: " + name);
}
return c;
}
function fromNumber(val, base256 = false) {
if (val < 0) {
return new Color();
}
else if (base256 || val > 0xfff) {
return new Color(Math.round((((val & 0xff0000) >> 16) * 100) / 255), Math.round((((val & 0xff00) >> 8) * 100) / 255), Math.round(((val & 0xff) * 100) / 255), 100);
}
else {
return new Color(Math.round((((val & 0xf00) >> 8) * 100) / 15), Math.round((((val & 0xf0) >> 4) * 100) / 15), Math.round(((val & 0xf) * 100) / 15), 100);
}
}
function make(...args) {
let arg = args[0];
let base256 = args[1];
if (args.length == 0)
return new Color();
if (args.length > 2) {
arg = args;
base256 = false; // TODO - Change this!!!
}
if (arg === undefined || arg === null)
return new Color(-1);
if (arg instanceof Color) {
return arg;
}
if (typeof arg === "string") {
if (arg.startsWith("#")) {
return fromCss(arg);
}
return fromName(arg);
}
else if (Array.isArray(arg)) {
return fromArray(arg, base256);
}
else if (typeof arg === "number") {
return fromNumber(arg, base256);
}
throw new Error("Failed to make color - unknown argument: " + JSON.stringify(arg));
}
function from(...args) {
const arg = args[0];
if (arg instanceof Color)
return arg;
if (arg === undefined)
return new Color(-1);
if (typeof arg === "string") {
if (!arg.startsWith("#")) {
return fromName(arg);
}
}
return make(arg, args[1]);
}
// adjusts the luminosity of 2 colors to ensure there is enough separation between them
function separate(a, b) {
if (a.isNull() || b.isNull())
return [a, b];
const A = a.clamp();
const B = b.clamp();
// console.log('separate');
// console.log('- a=%s, h=%d, s=%d, l=%d', A.toString(), A.h, A.s, A.l);
// console.log('- b=%s, h=%d, s=%d, l=%d', B.toString(), B.h, B.s, B.l);
let hDiff = Math.abs(A.h - B.h);
if (hDiff > 180) {
hDiff = 360 - hDiff;
}
if (hDiff > 45)
return [A, B]; // colors are far enough apart in hue to be distinct
const dist = 40;
if (Math.abs(A.l - B.l) >= dist)
return [A, B];
// Get them sorted by saturation ( we will darken the more saturated color and lighten the other)
const out = [A, B];
const lo = A.s <= B.s ? 0 : 1;
const hi = 1 - lo;
// console.log('- lo=%s, hi=%s', lo.toString(), hi.toString());
while (out[hi].l - out[lo].l < dist) {
out[hi] = out[hi].mix(WHITE, 5);
out[lo] = out[lo].mix(BLACK, 5);
}
// console.log('=>', a.toString(), b.toString());
return out;
}
function relativeLuminance(a, b) {
return Math.round((100 *
((a.r - b.r) * (a.r - b.r) * 0.2126 +
(a.g - b.g) * (a.g - b.g) * 0.7152 +
(a.b - b.b) * (a.b - b.b) * 0.0722)) /
10000);
}
function distance(a, b) {
return Math.round((100 *
((a.r - b.r) * (a.r - b.r) * 0.3333 +
(a.g - b.g) * (a.g - b.g) * 0.3333 +
(a.b - b.b) * (a.b - b.b) * 0.3333)) /
10000);
}
// Draws the smooth gradient that appears on a button when you hover over or depress it.
// Returns the percentage by which the current tile should be averaged toward a hilite color.
function smoothScalar(rgb, maxRgb = 100) {
return Math.floor(100 * Math.sin((Math.PI * rgb) / maxRgb));
}
function install(name, ...args) {
let info = args;
if (args.length == 1) {
info = args[0];
}
const c = info instanceof Color ? info : make(info);
// @ts-ignore
c._const = true;
colors[name] = c;
c.name = name;
return c;
}
function installSpread(name, ...args) {
let c;
if (args.length == 1) {
c = install(name, args[0]);
}
else {
c = install(name, ...args);
}
install("light_" + name, c.lighten(25));
install("lighter_" + name, c.lighten(50));
install("lightest_" + name, c.lighten(75));
install("dark_" + name, c.darken(25));
install("darker_" + name, c.darken(50));
install("darkest_" + name, c.darken(75));
return c;
}
const NONE = install("NONE", -1);
const BLACK = install("black", 0x000);
const WHITE = install("white", 0xfff);
installSpread("teal", [30, 100, 100]);
installSpread("brown", [60, 40, 0]);
installSpread("tan", [80, 70, 55]); // 80, 67, 15);
installSpread("pink", [100, 60, 66]);
installSpread("gray", [50, 50, 50]);
installSpread("yellow", [100, 100, 0]);
installSpread("purple", [100, 0, 100]);
installSpread("green", [0, 100, 0]);
installSpread("orange", [100, 50, 0]);
installSpread("blue", [0, 0, 100]);
installSpread("red", [100, 0, 0]);
installSpread("amber", [100, 75, 0]);
installSpread("flame", [100, 25, 0]);
installSpread("fuchsia", [100, 0, 100]);
installSpread("magenta", [100, 0, 75]);
installSpread("crimson", [100, 0, 25]);
installSpread("lime", [75, 100, 0]);
installSpread("chartreuse", [50, 100, 0]);
installSpread("sepia", [50, 40, 25]);
installSpread("violet", [50, 0, 100]);
installSpread("han", [25, 0, 100]);
installSpread("cyan", [0, 100, 100]);
installSpread("turquoise", [0, 100, 75]);
installSpread("sea", [0, 100, 50]);
installSpread("sky", [0, 75, 100]);
installSpread("azure", [0, 50, 100]);
installSpread("silver", [75, 75, 75]);
installSpread("gold", [100, 85, 0]);
var color = /*#__PURE__*/Object.freeze({
__proto__: null,
colors: colors,
Color: Color,
fromArray: fromArray,
fromCss: fromCss,
fromName: fromName,
fromNumber: fromNumber,
make: make,
from: from,
separate: separate,
relativeLuminance: relativeLuminance,
distance: distance,
smoothScalar: smoothScalar,
install: install,
installSpread: installSpread,
NONE: NONE,
BLACK: BLACK,
WHITE: WHITE
});
class Layer {
constructor(canvas, depth = 0) {
this._empty = true;
this.canvas = canvas;
this.resize(canvas.width, canvas.height);
this._depth = depth;
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get depth() {
return this._depth;
}
get empty() {
return this._empty;
}
detach() {
// @ts-ignore
this.canvas = null;
}
resize(width, height) {
const size = width * height * VERTICES_PER_TILE;
if (!this.fg || this.fg.length !== size) {
this.fg = new Uint16Array(size);
this.bg = new Uint16Array(size);
this.glyph = new Uint8Array(size);
}
}
clear() {
this.fg.fill(0);
this.bg.fill(0);
this.glyph.fill(0);
this._empty = true;
}
draw(x, y, glyph, fg = 0xfff, bg = -1) {
const index = x + y * this.canvas.width;
if (typeof glyph === "string") {
glyph = this.canvas.glyphs.forChar(glyph);
}
fg = from(fg).toInt();
bg = from(bg).toInt();
this.set(index, glyph, fg, bg);
if (glyph || bg || fg) {
this._empty = false;
this.canvas._requestRender();
}
}
set(index, glyph, fg, bg) {
index *= VERTICES_PER_TILE;
glyph = glyph & 0xff;
bg = bg & 0xffff;
fg = fg & 0xffff;
for (let i = 0; i < VERTICES_PER_TILE; ++i) {
this.glyph[index + i] = glyph;
this.fg[index + i] = fg;
this.bg[index + i] = bg;
}
}
// forEach(
// cb: (i: number, glyph: number, fg: number, bg: number) => void
// ): void {
// for (let i = 0; i < this.glyph.length; ++i) {
// cb(i, this.glyph[i], this.fg[i], this.bg[i]);
// }
// }
copy(buffer) {
if (buffer.width !== this.width || buffer.height !== this.height) {
console.log("auto resizing buffer");
buffer.resize(this.width, this.height);
}
if (!this.canvas) {
throw new Error("Layer is detached. Did you resize the canvas?");
}
buffer._data.forEach((mixer, i) => {
let glyph = mixer.ch;
if (typeof glyph === "string") {
glyph = this.canvas.glyphs.forChar(glyph);
}
this.set(i, glyph, mixer.fg.toInt(), mixer.bg.toInt());
});
this._empty = false;
this.canvas._requestRender();
}
copyTo(buffer) {
buffer.resize(this.width, this.height);
for (let y = 0; y < this.height; ++y) {
for (let x = 0; x < this.width; ++x) {
const index = (x + y * this.width) * VERTICES_PER_TILE;
buffer.draw(x, y, this.glyph[index], this.fg[index], this.bg[index]);
}
}
}
}
// Based on: https://github.com/ondras/fastiles/blob/master/ts/scene.ts (v2.1.0)
const VERTICES_PER_TILE = 6;
class NotSupportedError extends Error {
constructor(...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
// @ts-ignore
if (Error.captureStackTrace) {
// @ts-ignore
Error.captureStackTrace(this, NotSupportedError);
}
this.name = "NotSupportedError";
}
}
class Canvas {
constructor(options) {
this._renderRequested = false;
this._autoRender = true;
this._width = 50;
this._height = 25;
this._layers = [];
if (!options.glyphs)
throw new Error("You must supply glyphs for the canvas.");
this._node = this._createNode();
this._createContext();
this._configure(options);
}
get node() {
return this._node;
}
get width() {
return this._width;
}
get height() {
return this._height;
}
get tileWidth() {
return this._glyphs.tileWidth;
}
get tileHeight() {
return this._glyphs.tileHeight;
}
get pxWidth() {
return this.node.clientWidth;
}
get pxHeight() {
return this.node.clientHeight;
}
get glyphs() {
return this._glyphs;
}
set glyphs(glyphs) {
this._setGlyphs(glyphs);
}
layer(depth = 0) {
let layer = this._layers.find((l) => l.depth === depth);
if (layer)
return layer;
layer = new Layer(this, depth);
this._layers.push(layer);
this._layers.sort((a, b) => a.depth - b.depth);
return layer;
}
clearLayer(depth = 0) {
const layer = this._layers.find((l) => l.depth === depth);
if (layer)
layer.clear();
}
removeLayer(depth = 0) {
const index = this._layers.findIndex((l) => l.depth === depth);
if (index > -1) {
this._layers.splice(index, 1);
}
}
_createNode() {
return document.createElement("canvas");
}
_configure(options) {
this._width = options.width || this._width;
this._height = options.height || this._height;
this._autoRender = options.render !== false;
this._setGlyphs(options.glyphs);
this.bg = from(options.bg || BLACK);
if (options.div) {
let el;
if (typeof options.div === "string") {
el = document.getElementById(options.div);
if (!el) {
console.warn("Failed to find parent element by ID: " + options.div);
}
}
else {
el = options.div;
}
if (el && el.appendChild) {
el.appendChild(this.node);
}
}
}
_setGlyphs(glyphs) {
if (glyphs === this._glyphs)
return false;
this._glyphs = glyphs;
this.resize(this._width, this._height);
const gl = this._gl;
const uniforms = this._uniforms;
gl.uniform2uiv(uniforms["tileSize"], [this.tileWidth, this.tileHeight]);
this._uploadGlyphs();
return true;
}
resize(width, height) {
this._width = width;
this._height = height;
const node = this.node;
node.width = this._width * this.tileWidth;
node.height = this._height * this.tileHeight;
const gl = this._gl;
// const uniforms = this._uniforms;
gl.viewport(0, 0, this.node.width, this.node.height);
// gl.uniform2ui(uniforms["viewportSize"], this.node.width, this.node.height);
this._createGeometry();
this._createData();
}
_requestRender() {
if (this._renderRequested)
return;
this._renderRequested = true;
if (!this._autoRender)
return;
requestAnimationFrame(() => this.render());
}
hasXY(x, y) {
return x >= 0 && y >= 0 && x < this.width && y < this.height;
}
toX(x) {
return Math.floor((this.width * x) / this.node.clientWidth);
}
toY(y) {
return Math.floor((this.height * y) / this.node.clientHeight);
}
_createContext() {
let gl = this.node.getContext("webgl2");
if (!gl) {
throw new NotSupportedError("WebGL 2 not supported");
}
this._gl = gl;
this._buffers = {};
this._attribs = {};
this._uniforms = {};
const p = createProgram(gl, VS, FS);
gl.useProgram(p);
const attributeCount = gl.getProgramParameter(p, gl.ACTIVE_ATTRIBUTES);
for (let i = 0; i < attributeCount; i++) {
gl.enableVertexAttribArray(i);
let info = gl.getActiveAttrib(p, i);
this._attribs[info.name] = i;
}
const uniformCount = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < uniformCount; i++) {
let info = gl.getActiveUniform(p, i);
this._uniforms[info.name] = gl.getUniformLocation(p, info.name);
}
gl.uniform1i(this._uniforms["font"], 0);
this._texture = createTexture(gl);
}
_createGeometry() {
const gl = this._gl;
this._buffers.position && gl.deleteBuffer(this._buffers.position);
this._buffers.uv && gl.deleteBuffer(this._buffers.uv);
let buffers = createGeometry(gl, this._attribs, this.width, this.height);
Object.assign(this._buffers, buffers);
}
_createData() {
const gl = this._gl;
const attribs = this._attribs;
this._buffers.fg && gl.deleteBuffer(this._buffers.fg);
this._buffers.bg && gl.deleteBuffer(this._buffers.bg);
this._buffers.glyph && gl.deleteBuffer(this._buffers.glyph);
if (this._layers.length) {
this._layers.forEach((l) => l.detach());
this._layers.length = 0;
}
const fg = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, fg);
gl.vertexAttribIPointer(attribs["fg"], 1, gl.UNSIGNED_SHORT, 0, 0);
const bg = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bg);
gl.vertexAttribIPointer(attribs["bg"], 1, gl.UNSIGNED_SHORT, 0, 0);
const glyph = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, glyph);
gl.vertexAttribIPointer(attribs["glyph"], 1, gl.UNSIGNED_BYTE, 0, 0);
Object.assign(this._buffers, { fg, bg, glyph });
}
_uploadGlyphs() {
if (!this._glyphs.needsUpdate)
return;
const gl = this._gl;
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this._texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._glyphs.node);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
this._requestRender();
this._glyphs.needsUpdate = false;
}
draw(x, y, glyph, fg, bg) {
this.layer(0).draw(x, y, glyph, fg, bg);
}
render() {
const gl = this._gl;
if (this._glyphs.needsUpdate) {
// auto keep glyphs up to date
this._uploadGlyphs();
}
else if (!this._renderRequested) {
return;
}
this._renderRequested = false;
// clear to bg color?
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(this.bg.r / 100, this.bg.g / 100, this.bg.b / 100, this.bg.a / 100);
gl.clear(gl.COLOR_BUFFER_BIT);
// sort layers?
this._layers.forEach((layer) => {
if (layer.empty)
return;
// set depth
gl.uniform1i(this._uniforms["depth"], layer.depth);
gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.fg);
gl.bufferData(gl.ARRAY_BUFFER, layer.fg, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.bg);
gl.bufferData(gl.ARRAY_BUFFER, layer.bg, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, this._buffers.glyph);
gl.bufferData(gl.ARRAY_BUFFER, layer.glyph, gl.DYNAMIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, this._width * this._height * VERTICES_PER_TILE);
});
}
}
function withImage(image) {
let opts = {};
if (typeof image === "string") {
opts.glyphs = Glyphs.fromImage(image);
}
else if (image instanceof HTMLImageElement) {
opts.glyphs = Glyphs.fromImage(image);
}
else {
if (!image.image)
throw new Error("You must supply the image.");
Object.assign(opts, image);
opts.glyphs = Glyphs.fromImage(image.image);
}
return new Canvas(opts);
}
function withFont(src) {
if (typeof src === "string") {
src = { font: src };
}
src.glyphs = Glyphs.fromFont(src);
return new Canvas(src);
}
// Copy of: https://github.com/ondras/fastiles/blob/master/ts/utils.ts (v2.1.0)
function createProgram(gl, ...sources) {
const p = gl.createProgram();
[gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, index) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, sources[index]);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(shader));
}
gl.attachShader(p, shader);
});
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(p));
}
return p;
}
function createTexture(gl) {
let t = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, t);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
return t;
}
// x, y offsets for 6 verticies (2 triangles) in square
const QUAD = [0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1];
function createGeometry(gl, attribs, width, height) {
let tileCount = width * height;
let positionData = new Float32Array(tileCount * QUAD.length);
let offsetData = new Uint8Array(tileCount * QUAD.length);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (x + y * width) * QUAD.length;
positionData.set(QUAD.map((v, i) => {
if (i % 2) {
// y
return 1 - (2 * (y + v)) / height;
}
else {
return (2 * (x + v)) / width - 1;
}
}), index);
offsetData.set(QUAD, index);
}
}
const position = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, position);
gl.vertexAttribPointer(attribs["position"], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
const uv = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uv);
gl.vertexAttribIPointer(attribs["offset"], 2, gl.UNSIGNED_BYTE, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, offsetData, gl.STATIC_DRAW);
return { position, uv };
}
class Mixer {
constructor(base = {}) {
this.ch = base.ch || 0;
this.fg = make(base.fg);
this.bg = make(base.bg);
}
_changed() {
return this;
}
copy(other) {
this.ch = other.ch || -1;
this.fg = from(other.fg);
this.bg = from(other.bg);
return this._changed();
}
clone() {
const other = new Mixer();
other.copy(this);
return other;
}
equals(other) {
return (this.ch == other.ch &&
this.fg.equals(other.fg) &&
this.bg.equals(other.bg));
}
// get dances(): boolean {
// return this.fg.dances || this.bg.dances;
// }
nullify() {
this.ch = -1;
this.fg = NONE;
this.bg = NONE;
return this._changed();
}
blackOut() {
this.ch = -1;
this.fg = BLACK;
this.bg = BLACK;
return this._changed();
}
draw(ch = -1, fg = -1, bg = -1) {
if (ch && ch !== -1) {
this.ch = ch;
}
if (fg !== -1 && fg !== null) {
fg = from(fg);
this.fg = this.fg.blend(fg);
}
if (bg !== -1 && bg !== null) {
bg = from(bg);
this.bg = this.bg.blend(bg);
}
return this._changed();
}
drawSprite(src, opacity) {
if (src === this)
return this;
// @ts-ignore
if (opacity === undefined)
opacity = src.opacity;
if (opacity === undefined)
opacity = 100;
if (opacity <= 0)
return;
if (src.ch)
this.ch = src.ch;
if ((src.fg && src.fg !== -1) || src.fg === 0)
this.fg = this.fg.mix(src.fg, opacity);
if ((src.bg && src.bg !== -1) || src.bg === 0)
this.bg = this.bg.mix(src.bg, opacity);
return this._changed();
}
invert() {
[this.bg, this.fg] = [this.fg, this.bg];
return this._changed();
}
multiply(color$1, fg = true, bg = true) {
color$1 = from(color$1);
if (fg) {
this.fg = this.fg.multiply(color$1);
}
if (bg) {
this.bg = this.bg.multiply(color$1);
}
return this._changed();
}
scale(multiplier, fg = true, bg = true) {
if (fg)
this.fg = this.fg.scale(multiplier);
if (bg)
this.bg = this.bg.scale(multiplier);
return this._changed();
}
mix(color$1, fg = 50, bg = fg) {
color$1 = from(color$1);
if (fg > 0) {
this.fg = this.fg.mix(color$1, fg);
}
if (bg > 0) {
this.bg = this.bg.mix(color$1, bg);
}
return this._changed();
}
add(color$1, fg = 100, bg = fg) {
color$1 = from(color$1);
if (fg > 0) {
this.fg = this.fg.add(color$1, fg);
}
if (bg > 0) {
this.bg = this.bg.add(color$1, bg);
}
return this._changed();
}
separate() {
[this.fg, this.bg] = separate(this.fg, this.bg);
return this._changed();
}
bake(clearDancing = false) {
this.fg = this.fg.bake(clearDancing);
this.bg = this.bg.bake(clearDancing);
this._changed();
return {
ch: this.ch,
fg: this.fg.toInt(),
bg: this.bg.toInt(),
};
}
toString() {
// prettier-ignore
return `{ ch: ${this.ch}, fg: ${this.fg.toString()}, bg: ${this.bg.toString()} }`;
}
}
class DataBuffer {
constructor(width, height) {
this._data = [];
this.resize(width, height);
}
get width() {
return this._width;
}
get height() {
return this._height;
}
resize(width, height) {
if (this._width === width && this._height === height)
return;
this._width = width;
this._height = height;
while (this._data.length < width * height) {
this._data.push(new Mixer());
}
this._data.length = width * height; // truncate if was too large
}
get(x, y) {
let index = y * this.width + x;
return this._data[index];
}
_toGlyph(ch) {
if (ch === null || ch === undefined)
return -1;
return ch.charCodeAt(0);
}
draw(x, y, glyph = -1, fg = -1, bg = -1) {
let index = y * this.width + x;
const current = this._data[index];
current.draw(glyph, fg, bg);
return this;
}
// This is without opacity - opacity must be done in Mixer
drawSprite(x, y, sprite) {
let glyph = sprite.ch
? sprite.ch
: sprite.glyph !== undefined
? sprite.glyph
: -1;
// const fg = sprite.fg ? sprite.fg.toInt() : -1;
// const bg = sprite.bg ? sprite.bg.toInt() : -1;
return this.draw(x, y, glyph, sprite.fg, sprite.bg);
}
blackOut(x, y) {
if (arguments.length == 0) {
return this.fill(0, 0, 0);
}
return this.draw(x, y, 0, 0, 0);
}
fill(glyph = 0, fg = 0xfff, bg = 0) {
this._data.forEach((m) => m.draw(glyph, fg, bg));
return this;
}
copy(other) {
this._data.forEach((m, i) => {
m.copy(other._data[i]);
});
return this;
}
}
class Buffer extends DataBuffer {
constructor(layer) {
super(layer.width, layer.height);
this._layer = layer;
layer.copyTo(this);
}
// get canvas() { return this._canvas; }
_toGlyph(ch) {
return this._layer.canvas.glyphs.forChar(ch);
}
render() {
this._layer.copy(this);
return this;
}
copyFromLayer() {
this._layer.copyTo(this);
return this;
}
}
var options = {
random: Math.random.bind(Math),
};
function configure(opts = {}) {
Object.assign(options, opts);
}
function clamp(v, n, x) {
return Math.min(x, Math.max(n, v));
}
class Sprite {
constructor(ch, fg, bg, opacity = 100) {
if (!ch)
ch = null;
this.ch = ch;
this.fg = from(fg);
this.bg = from(bg);
this.opacity = clamp(opacity, 0, 100);
}
clone() {
return new Sprite(this.ch, this.fg, this.bg, this.opacity);
}
toString() {
const parts = [];
if (this.ch)
parts.push("ch: " + this.ch);
if (!this.fg.isNull())
parts.push("fg: " + this.fg.toString());
if (!this.bg.isNull())
parts.push("bg: " + this.bg.toString());
if (this.opacity !== 100)
parts.push("opacity: " + this.opacity);
return "{ " + parts.join(", ") + " }";
}
}
export { Buffer, Canvas, DataBuffer, Glyphs, Layer, Mixer, NotSupportedError, Sprite, color, configure, withFont, withImage };