lib/BasicCanvas.js
// Basic semi-related tools.
export const clone = obj => Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);
Object.prototype.clone = function () {
return clone(this);
};
export const type = element => (
({}).toString.call(element).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
);
// --> Try to make `use()`, `type()` and `clone()` functions global.
let _use;
if (typeof window === 'undefined') {
_use = (namespace, global) => Object.assign(global, namespace);
} else {
_use = (namespace, global = window) => Object.assign(global, namespace);
window.use = _use;
window.type = type;
window.clone = clone;
}
export const use = _use;
export const load_font = (name, path, description) => {
const font = new FontFace(name, path, description);
font.load().then(loaded => document.fonts.add(loaded));
return font;
};
export const plain = (...args) => String.raw({raw: args[0]}, ...args.slice(1));
export const style = string => {
const node = document.createElement('style');
node.innerHTML = string;
document.body.appendChild(node);
};
export const css = (s, ...exps) => style(plain(s, ...exps));
// Patching and Monkey Patching prototypes.
Math.TAU = 2 * Math.PI;
Math.HALF_PI = Math.PI * 0.5;
Number.prototype.roundTo = function (dp) {
return parseFloat((this).toFixed(dp));
};
Array.prototype.mag = function () {
return Math.sqrt(this.reduce((i, j) => i + j ** 2, 0));
};
Array.prototype.norm = function () {
if (this.every(e => e === 0)) {
return this;
}
return this.map(e => e / this.mag());
};
Array.prototype.rotate = function (theta, origin=[0,0]) {
return [
origin[0] + (this[0] - origin[0]) * Math.cos(theta) - (this[1] - origin[1]) * Math.sin(theta),
origin[1] + (this[0] - origin[0]) * Math.sin(theta) + (this[1] - origin[1]) * Math.cos(theta)
];
};
String.prototype.replaceAll = function (search, replacement) {
return this.replace(new RegExp(search, 'g'), replacement);
};
HTMLElement.prototype.html = function (s, ...exps) {
const contain = document.createElement('del');
contain.style.textDecoration = 'none';
contain.innerHTML = String.raw(s, ...exps);
this.appendChild(contain);
};
HTMLElement.prototype.css = function (properties) {
for (const property in properties) {
if (Object.prototype.hasOwnProperty.call(properties, property)) {
this.style[property] = properties[property];
}
}
};
Object.prototype.omap = function (lambda) {
return Object.assign({}, ...Object.keys(this).map(k => ({[k]: lambda(this[k])})));
};
Object.defineProperty(HTMLElement.prototype, 'elem', {
get: function elem() {
return this;
}
});
// More interaction-specific tools
export const click = (handler, canvas = null) => {
if (canvas) {
canvas.elem.addEventListener('click', handler, false);
} else {
window.addEventListener('click', handler, false);
}
};
export const mouse_down = (handler, canvas = null) => {
if (canvas) {
canvas.elem.addEventListener('mousedown', handler, false);
} else {
window.addEventListener('mousedown', handler, false);
}
};
export const mouse_up = (handler, canvas = null) => {
if (canvas) {
canvas.elem.addEventListener('mouseup', handler, false);
} else {
window.addEventListener('mouseup', handler, false);
}
};
export const key_press = handler => {
window.addEventListener('keypress', handler, false);
};
export const key_down = handler => {
window.addEventListener('keydown', handler, false);
};
export const key_up = handler => {
window.addEventListener('keyup', handler, false);
};
// Classes for specific data-types
class PointObj {
constructor(x, y) {
this.x = x;
this.y = y;
}
get array() {
return [this.x, this.y];
}
set array(a) {
[this.x, this.y] = a;
}
floor() {
return new PointObj(Math.floor(this.x), Math.floor(this.y));
}
norm(other = new PointObj(0, 0)) {
return new PointObj(...this.sub(other).array.norm());
}
unit(...args) {
return this.norm(...args);
}
sum() {
return this.x + this.y;
}
add(other) {
return new PointObj(this.x + other.x, this.y + other.y);
}
offset(x, y) {
return new PointObj(this.x + x, this.y + y);
}
sub(other) {
return new PointObj(this.x - other.x, this.y - other.y);
}
scale(scalar) {
return new PointObj(this.x * scalar, this.y * scalar);
}
mul(other) {
if (typeof (other) === 'number') {
return this.scale(other);
}
return new PointObj(this.x * other.x, this.y * other.y);
}
dot(other) {
const standard = this.mul(other);
return standard.sum();
}
div(other) {
if (typeof (other) !== 'number') {
throw new TypeError('Can only divide vectors by numerics.');
}
return new PointObj(this.x / other, this.y / other);
}
mag(other = new PointObj(0, 0)) {
return this.sub(other).array.mag();
}
size(...xs) {
return this.mag(...xs);
}
length(...xs) {
return this.mag(...xs);
}
modulus(...xs) {
return this.mag(...xs);
}
angle(other = new PointObj(0, 0)) {
const v = this.sub(other);
return Math.atan2(v.y, v.x);
}
phase(...xs) {
return this.angle(...xs);
}
arg(...xs) {
return this.angle(...xs);
}
rotate(theta, origin = new PointObj(0, 0)) {
return new PointObj(...this.array.rotate(theta, origin.array));
}
toString() {
return `(${this.x}, ${this.y})`;
}
valueOf() {
return this.toString();
}
}
class RGBAObj {
constructor(r, g, b, a) {
[this.r, this.g, this.b, this.a] = [r, g, b, a].map(Math.round);
this.rgba = [this.r, this.g, this.b, this.a];
this.rgb = this.rgba.slice(0, -1);
}
toString() {
return `rgba(${this.rgb.join(', ')}, ${this.a / 255})`;
}
valueOf() {
return this.toString();
}
}
export const Color = (r, g = -1, b = -1, a = 255) => {
if (type(r) === 'string') {
return new NamedColorObj(r);
}
if (type(r) === 'array') {
return new NamedColorObj(r[0]);
}
if (b < 0 && g >= 0) {
a = g;
}
if (b < 0) {
[g, b] = [r, r];
}
return new RGBAObj(r, g, b, a);
};
export const Colour = Color;
export const RGBA = Color;
export const RGB = Color;
function hue_to_rgb(p, q, t) {
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
class HSLObj {
constructor(h, s, l, a) {
[this.h, this.s, this.l, this.a] = [h, s, l, a].map(Math.round);
this.rgb = this.to_rgb();
}
to_rgb() {
let r, g, b;
const h = this.h / 360;
const [s, l] = [this.s, this.l].map(n => n / 100);
if (s == 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue_to_rgb(p, q, h + 1 / 3);
g = hue_to_rgb(p, q, h);
b = hue_to_rgb(p, q, h - 1 / 3);
}
return RGBA(r * 255, g * 255, b * 255, this.a);
}
toString() {
return `hsla(${this.h}, ${this.s}%, ${this.l}%, ${this.a / 255})`;
}
valueOf() {
return this.toString();
}
}
class HSVObj {
constructor(h, s, v, a) {
[this.h, this.s, this.v, this.a] = [h, s, v, a].map(Math.round);
this.rgb = this.to_rgb();
}
to_rgb() {
let r, g, b;
const h = this.h / 360;
const [s, v] = [this.s, this.v].map(n => n / 100);
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return RGBA(r * 255, g * 255, b * 255, this.a);
}
toString() {
const [r, g, b, a] = this.to_rgb().rgba;
return `rgba(${r}, ${g}, ${b}, ${a / 255})`;
}
valueOf() {
return this.toString();
}
}
class HEXobj {
constructor(hex) {
const chars = '000000ff'.split(''); // 0x000000ff
let given;
if (type(hex) === 'string') {
given = hex.split('#').slice(-1)[0];
} else if (type(hex) === 'array') {
given = plain(...hex).split('#').slice(-1)[0];
} else {
given = hex.toString(16);
}
if (given.length === 3 || given.length === 4) {
chars[0] = given[0];
chars[1] = given[0];
chars[2] = given[1];
chars[3] = given[1];
chars[4] = given[2];
chars[5] = given[2];
if (given.length === 4) {
chars[6] = given[3];
chars[7] = given[3];
}
} else if (given.length === 6 || given.length === 8) {
chars[0] = given[0];
chars[1] = given[1];
chars[2] = given[2];
chars[3] = given[3];
chars[4] = given[4];
chars[5] = given[5];
if (given.length === 8) {
chars[6] = given[6];
chars[7] = given[7];
}
} else {
throw new Error('Invalid hex format: ' + given);
}
const str = chars.join('');
this.str = '#' + str;
this.hex = parseInt(str, 16);
this.rgb = this.to_rgb();
}
to_rgb() {
const r = this.hex >> 24 & 0xFF;
const g = this.hex >> 16 & 0xFF;
const b = this.hex >> 8 & 0xFF;
const a = this.hex & 0xFF;
return RGBA(r, g, b, a);
}
toString() {
return this.str;
}
valueOf() {
return this.toString();
}
}
export const NAMED_COLORS = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
goldenrod: '#daa520',
gold: '#ffd700',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavenderblush: '#fff0f5',
lavender: '#e6e6fa',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
transparent: '#00000000',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32'
}.omap(c => (new HEXobj(c)).rgb);
class NamedColorObj {
constructor(color) {
this.color = color;
this.rgb = this.to_rgb();
}
to_rgb() {
return NAMED_COLORS[this.color];
}
toString() {
return this.color;
}
valueOf() {
return this.toString();
}
}
// Construction functions for data-types
export const Point = (x, y) => new PointObj(x, y);
export const Polar = (r, theta, origin = Point(0, 0)) => Point(
r * Math.cos(theta) + origin.x,
r * Math.sin(theta) + origin.y
);
export const [P, point, polar] = [Point, Point, Polar];
export const HEX = hex => new HEXobj(hex);
export const HSL = (h, s = 100, l = 50, a = 255) => new HSLObj(h, s, l, a);
export const HSLA = HSL;
export const HSV = (h, s = 100, v = 100, a = 255) => new HSVObj(h, s, v, a);
export const HSVA = HSV;
export const TRANSPARENT = Object.freeze('transparent');
const rgb_regex = /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i;
const hsl_regex = /^hsl\s*\(\s*(\d+)\s*,\s*(\d+(\%)?)\s*,\s*(\d+(\%)?)\s*\)$/i;
const rgba_regex = /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([+-]?\d+(\.\d+)?)\s*\)$/i;
const hsla_regex = /^hsla\s*\(\s*(\d+)\s*,\s*(\d+(\%)?)\s*,\s*(\d+(\%)?)\s*,\s*([+-]?\d+(\.\d+)?)\s*\)$/i;
const hex_regex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
const trans_regex = /^transparent$/i;
export const to_rgb = c => {
let match = null;
if (type(c) === 'string') {
// Check rgb
match = (
c.match(rgb_regex) ||
c.match(rgba_regex)
);
if (match) {
if (match[4]) {
alpha = 255 * parseFloat(match[4]);
}
return RGBA(...([match[1], match[2], match[3]].map(parseFloat)), alpha);
}
// Check hsl
match = (
c.match(hsl_regex) ||
c.match(hsla_regex)
);
if (match) {
if (match[4]) {
alpha = 255 * parseFloat(match[4]);
}
return HSLA(...([match[1], match[2], match[3]].map(parseFloat)), alpha).rgb;
}
// Check hex
match = c.match(hex_regex);
if (match) {
return HEX(c).rgb;
}
return Color(c).rgb;
}
return c.rgb;
};
export const to_rgba = to_rgb;
// Implements and manages every rendered shape seen.
class Shape {
constructor(name, canvas) {
this.name = name;
this.canvas = canvas;
this.primitive = null;
this.vertices = [];
this.center = null;
this.hollow = false;
this._first_stroke_color = null;
this._stroke_changes = false;
}
flesh() {
this.canvas.context.fill();
this.canvas.context.stroke();
}
style(
fill = this.canvas.fill,
stroke = this.canvas.stroke,
stroke_weight = this.canvas.stroke_weight,
stroke_cap = this.canvas.stroke_cap
) {
if (stroke_weight === 0) {
stroke = TRANSPARENT;
}
const c = this.canvas.context;
c.fillStyle = (this.hollow) ? TRANSPARENT : fill.toString();
c.strokeStyle = stroke.toString();
c.lineWidth = stroke_weight;
c.lineCap = stroke_cap;
return this;
}
point(point, color = this.canvas.stroke) {
return this.canvas.color(point, color);
}
vertex(point, y = null) {
if (y !== null) {
if (Number.isNaN(point) || Number.isNaN(y)) {
return;
}
point = Point(point, y);
} else if (Number.isNaN(point.x) || Number.isNaN(point.y)) {
return;
}
if (this.vertices.length === 0) {
this.vertices.push([point.x, point.y]);
this.center = point;
return point;
}
const c = this.canvas.context;
c.beginPath();
c.moveTo(...this.vertices[this.vertices.length - 1]);
const next = [point.x, point.y];
c.lineTo(...next);
if (this._first_stroke_color === null) {
this._first_stroke_color = this.canvas.stroke.valueOf();
} else if (this._first_stroke_color !== this.canvas.stroke.valueOf()) {
this._stroke_changes = true;
this.style();
this.flesh();
}
this.vertices.push(next);
return point;
}
rect(point, w, h, fill = this.canvas.fill, stroke = this.canvas.stroke) {
this.style(fill, stroke);
const c = this.canvas.context;
this.primitive = () => c.rect(point.x, point.y, w, h);
}
ellipse(point, w, h, fill = this.canvas.fill, stroke = this.canvas.stroke) {
this.style(fill, stroke);
const c = this.canvas.context;
this.primitive = () => c.ellipse(point.x, point.y, w, h, 0, 0, Math.TAU);
}
close() {
this.vertex(...this.vertices[0]);
return this;
}
fill(color = null) {
let temp_color = color;
if (temp_color === null) {
temp_color = this.canvas.fill;
}
if (this.vertices.length < 2 && this.primitive === null) {
return;
}
if (this.hollow
|| (this.primitive === null && this.vertices.length < 3)
|| to_rgb(temp_color).a === 0) {
temp_color = TRANSPARENT;
}
const c = this.canvas.context;
c.beginPath();
if (this.primitive === null) {
if (this.vertices.length > 0) {
c.moveTo(...this.vertices[0]);
for (const vertex of this.vertices.slice(1)) {
c.lineTo(...vertex);
}
}
} else {
this.primitive();
this.style(temp_color);
this.flesh();
return;
}
let stroke = this.canvas.stroke;
if (this._stroke_changes) {
stroke = TRANSPARENT;
}
this.style(temp_color, stroke);
this.flesh();
}
stroke(color = this.canvas.stroke) {
this.style(RGBA(0, 0), color);
this.flesh();
}
render(...args) { this.fill(...args); }
rotate(theta, origin = this.center) {
for (let i = 0; i < this.vertices.length; i++) {
this.vertices[i] = this.vertices[i].rotate(theta, origin.array)
}
}
translate(x, y) {
for (let v of this.vertices) {
[v[0], v[1]] = [v[0] + x, v[1] + y]
}
}
scale(w, h = null, origin = this.center) {
if (h === null) h = w;
for (let v of this.vertices) {
[v[0], v[1]] = [
origin.x + w * (v[0] - origin.x),
origin.y + h * (v[1] - origin.y)
]
}
}
}
// Main Canvas class:
// --> First point of abstraction away from the standard canvas.
class Canvas {
constructor(elem) {
this.elem = elem;
this._width = this.elem.width;
this._height = this.elem.height;
// FPS variables.
this._now = null;
this._Δ = null;
this._then = Date.now();
this._interval = 1000 / 60;
// Canvas Context.
this.context = elem.getContext('2d');
this.image_data = this.context.getImageData(0, 0, this.width, this.height);
this.data = this.image_data.data;
// Main API properties.
this.fill = RGBA(255, 255, 255, 0);
this.stroke = RGB(0, 0, 0);
this._stroke_weight = 1;
this.stroke_cap = 'butt';
this.font = '16px sans-serif';
this.text_align = 'left';
this._mouse_position = Point(NaN, NaN);
this._mouse_listen = undefined;
// Used for coördinate calculations.
this.corner = {x: 0, y: 0};
this.stretch = [1, 1];
// Saved properties of the objects state at a certain time.
this.state_stack = [];
this.shapes = {}; // All shapes displayed on the canvas.
this.update = () => { }; // Lambda for when drawing a frame.
}
get FPS() {
return 1000 / this._Δ;
}
set FPS(frame_rate) {
this._interval = 1000 / frame_rate;
}
get width() {
return this._width;
}
get height() {
return this._height;
}
set width(w) {
this.elem.width = w;
this._width = w;
this.update_context();
}
set height(h) {
this.elem.height = h;
this._height = h;
this.update_context();
}
get stroke_weight() {
return this._stroke_weight / Math.max(...(this.stretch).map(e => Math.abs(e)));
}
set stroke_weight(w) {
if (w === 0) {
this.stroke = TRANSPARENT;
}
this._stroke_weight = w;
}
get mouse() {
if (this._mouse_listen === undefined) {
this._mouse_listen = this.elem.addEventListener('mousemove', evt => {
const rect = this.elem.getBoundingClientRect();
this._mouse_position = Point(
(evt.clientX - rect.left) / this.stretch[0] + this.corner.x,
(evt.clientY - rect.top) / this.stretch[1] + this.corner.y
);
});
}
return this._mouse_position;
}
update_context() {
this.context = this.elem.getContext('2d');
this.image_data = this.context.getImageData(0, 0, this.width, this.height);
this.data = this.image_data.data;
}
dimensions(w, h) {
this.width = w;
this.height = h;
this.update_context();
}
translate(x, y) {
[this.corner.x, this.corner.y] = [-x, -y];
this.context.translate(x, y);
}
rotate(theta) {
this.context.rotate(theta);
}
scale(x, y = x) {
[this.corner.x, this.corner.y] = [this.corner.x / x, this.corner.y / y];
this.stretch = [x, y];
this.context.scale(x, y);
}
unscale() {
this.scale(1 / this.stretch[0], 1 / this.stretch[1]);
}
save() {
const keys = Object.keys(this);
const saved = {};
for (const key of keys) {
if (this[key] !== null && typeof this[key] === 'object' && this[key].constructor === Object) {
saved[key] = Object.assign({}, this[key]);
} else if (key === 'state_stack') {
continue;
} else {
saved[key] = this[key];
}
}
this.state_stack.push(saved);
return this.context.save();
}
restore() {
const saved = this.state_stack.pop();
for (const key in saved) {
if (Object.prototype.hasOwnProperty.call(saved, key)) {
this[key] = saved[key];
}
}
return this.context.restore();
}
temp(λ) {
this.save();
λ();
return this.restore();
}
color(point, other = null) {
if (!other) {
return Color(...this.context.getImageData(point.x, point.y, 1, 1).data);
}
this.context.fillStyle = other.toString();
this.context.fillRect(
point.x, point.y,
1 / this.stretch[0], 1 / this.stretch[1]
);
return other;
}
point(point, color = this.stroke) {
return this.color(point, color);
}
shape(name, construction = null) {
let [_name, _construction] = [null, null];
if (construction === null && typeof name === 'function') {
_construction = name;
} else {
_construction = construction;
}
if (name === null || name === undefined || construction === null) {
_name = `ImplicitName${Object.keys(this.shapes).length}`;
} else {
_name = name;
}
const SHAPE = new Shape(_name, this);
this.shapes[_name] = {
draw: _construction,
shape: SHAPE
};
_construction(SHAPE);
return SHAPE;
}
render(...args) {
let shape = null;
if (args[0] instanceof Shape) shape = args[0];
else shape = this.shape(...args);
shape.fill();
return shape;
}
text(string, point, font = this.font, fill = this.fill, storke = this.stroke) {
const c = this.context;
c.font = font;
c.textAlign = this.text_align;
c.fillStyle = fill;
c.strokeStyle = storke;
c.fillText(string, point.x, point.y);
c.strokeText(string, point.x, point.y);
}
background(c = this.fill, clear = false) {
if (clear) {
this.context.clearRect(
this.corner.x,
this.corner.y,
-this.corner.x + Math.sign(this.stretch[0]) * this.width,
-this.corner.y + Math.sign(this.stretch[1]) * this.height
);
}
this.context.fillStyle = c.toString();
this.context.fillRect(
this.corner.x,
this.corner.y,
-this.corner.x + Math.sign(this.stretch[0]) * this.width,
-this.corner.y + Math.sign(this.stretch[1]) * this.height
);
}
update_frame(canvas) {
canvas.shapes = {};
window.requestAnimationFrame(() => {
canvas.update_frame(canvas);
});
canvas._now = Date.now();
canvas._Δ = canvas._now - canvas._then;
if (canvas._Δ > canvas._interval) {
canvas._then = canvas._now - (canvas._Δ % canvas._interval);
canvas.update(canvas.frame++);
}
}
loop(update) {
this.shapes = {};
this.update = update;
this.frame = 1;
window.requestAnimationFrame(() => {
this.update_frame(this);
});
}
}
// Create new `Canvas` instance in various ways.
export const canvas = elem => (
new Canvas(elem)
);
export const canvas_id = id => (
canvas(document.getElementById(id))
);
export const canvas_new = (id, parent_selector = 'body') => {
created = document.createElement('canvas');
created.id = id;
document.querySelector(parent_selector).appendChild(created);
return canvas_id(id);
};