gw-canvas
Version:
Library for rendering colorized bitmap fonts. Very fast, suitable for applications where the whole canvas needs frequent redrawing.
413 lines (412 loc) • 13.7 kB
JavaScript
export const colors = {};
function clamp(v, n, x) {
return Math.max(n, Math.min(x, v));
}
// All colors are const!!!
export 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(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(this.r, 0, 100),
clamp(this.g, 0, 100),
clamp(this.b, 0, 100),
clamp(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(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(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(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(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();
}
}
export 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);
}
export 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);
}
export function fromName(name) {
const c = colors[name];
if (!c) {
throw new Error("Unknown color name: " + name);
}
return c;
}
export 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);
}
}
export 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));
}
export 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
export 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;
}
export 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);
}
export 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.
export function smoothScalar(rgb, maxRgb = 100) {
return Math.floor(100 * Math.sin((Math.PI * rgb) / maxRgb));
}
export 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;
}
export 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;
}
export const NONE = install("NONE", -1);
export const BLACK = install("black", 0x000);
export 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]);