1209 lines
30 KiB
JavaScript
1209 lines
30 KiB
JavaScript
|
"use strict";
|
|||
|
import fs from "fs";
|
|||
|
import { EventEmitter } from "events";
|
|||
|
import { inspect } from "util";
|
|||
|
import { sync as glob, hasMagic } from "glob";
|
|||
|
import get from "simple-get";
|
|||
|
import geometry from "./geometry";
|
|||
|
import css from "./css";
|
|||
|
import io from "./io";
|
|||
|
const REPR = inspect.custom;
|
|||
|
// const fs = require("fs"),
|
|||
|
// { EventEmitter } = require("events"),
|
|||
|
// { inspect } = require("util"),
|
|||
|
// { sync: glob, hasMagic } = require("glob"),
|
|||
|
// get = require("simple-get"),
|
|||
|
// geometry = require("./geometry"),
|
|||
|
// css = require("./css"),
|
|||
|
// io = require("./io"),
|
|||
|
// REPR = inspect.custom;
|
|||
|
|
|||
|
//
|
|||
|
// Neon <-> Node interface
|
|||
|
//
|
|||
|
|
|||
|
const ø = Symbol.for("📦"), // the attr containing the boxed struct
|
|||
|
core = obj => (obj || {})[ø], // dereference the boxed struct
|
|||
|
wrap = (type, struct) => {
|
|||
|
// create new instance for struct
|
|||
|
let obj = internal(Object.create(type.prototype), ø, struct);
|
|||
|
return struct && internal(obj, "native", neon[type.name]);
|
|||
|
},
|
|||
|
neon = Object.entries(require("./v6")).reduce((api, [name, fn]) => {
|
|||
|
let [_, struct, getset, attr] = name.match(/(.*?)_(?:([sg]et)_)?(.*)/),
|
|||
|
cls = api[struct] || (api[struct] = {}),
|
|||
|
slot = getset ? cls[attr] || (cls[attr] = {}) : cls;
|
|||
|
slot[getset || attr] = fn;
|
|||
|
return api;
|
|||
|
}, {});
|
|||
|
|
|||
|
class RustClass {
|
|||
|
constructor(type) {
|
|||
|
internal(this, "native", neon[type.name]);
|
|||
|
}
|
|||
|
|
|||
|
alloc(...args) {
|
|||
|
return this.init("new", ...args);
|
|||
|
}
|
|||
|
|
|||
|
init(fn, ...args) {
|
|||
|
return internal(this, ø, this.native[fn](null, ...args));
|
|||
|
}
|
|||
|
|
|||
|
ref(key, val) {
|
|||
|
return arguments.length > 1
|
|||
|
? (this[Symbol.for(key)] = val)
|
|||
|
: this[Symbol.for(key)];
|
|||
|
}
|
|||
|
|
|||
|
prop(attr, val) {
|
|||
|
let getset = arguments.length > 1 ? "set" : "get";
|
|||
|
return this.native[attr][getset](this[ø], val);
|
|||
|
}
|
|||
|
|
|||
|
ƒ(fn, ...args) {
|
|||
|
try {
|
|||
|
return this.native[fn](this[ø], ...args);
|
|||
|
} catch (error) {
|
|||
|
Error.captureStackTrace(error, this.ƒ);
|
|||
|
throw error;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// shorthands for attaching read-only attributes
|
|||
|
const readOnly = (obj, attr, value) =>
|
|||
|
Object.defineProperty(obj, attr, {
|
|||
|
value,
|
|||
|
writable: false,
|
|||
|
enumerable: true
|
|||
|
});
|
|||
|
|
|||
|
const internal = (obj, attr, value) =>
|
|||
|
Object.defineProperty(obj, attr, {
|
|||
|
value,
|
|||
|
writable: false,
|
|||
|
enumerable: false
|
|||
|
});
|
|||
|
|
|||
|
// convert arguments list to a string of type abbreviations
|
|||
|
function signature(args) {
|
|||
|
return args
|
|||
|
.map(v =>
|
|||
|
Array.isArray(v)
|
|||
|
? "a"
|
|||
|
: { string: "s", number: "n", object: "o" }[typeof v] || "x"
|
|||
|
)
|
|||
|
.join("");
|
|||
|
}
|
|||
|
|
|||
|
const toString = val =>
|
|||
|
typeof val == "string" ? val : new String(val).toString();
|
|||
|
|
|||
|
//
|
|||
|
// Helpers to reconcile Skia and DOMMatrix’s disagreement about row/col orientation
|
|||
|
//
|
|||
|
|
|||
|
function toSkMatrix(jsMatrix) {
|
|||
|
if (Array.isArray(jsMatrix) && jsMatrix.length == 6) {
|
|||
|
var [a, b, c, d, e, f, m14, m24, m44] = jsMatrix.concat(0, 0, 1);
|
|||
|
} else if (jsMatrix instanceof geometry.DOMMatrix) {
|
|||
|
var { a, b, c, d, e, f, m14, m24, m44 } = jsMatrix;
|
|||
|
}
|
|||
|
return [a, c, e, b, d, f, m14, m24, m44];
|
|||
|
}
|
|||
|
|
|||
|
function fromSkMatrix(skMatrix) {
|
|||
|
let [a, b, c, d, e, f, p0, p1, p2] = skMatrix;
|
|||
|
return new geometry.DOMMatrix([
|
|||
|
a,
|
|||
|
d,
|
|||
|
0,
|
|||
|
p0,
|
|||
|
b,
|
|||
|
e,
|
|||
|
0,
|
|||
|
p1,
|
|||
|
0,
|
|||
|
0,
|
|||
|
1,
|
|||
|
0,
|
|||
|
c,
|
|||
|
f,
|
|||
|
0,
|
|||
|
p2
|
|||
|
]);
|
|||
|
}
|
|||
|
|
|||
|
//
|
|||
|
// The Canvas API
|
|||
|
//
|
|||
|
|
|||
|
class Canvas extends RustClass {
|
|||
|
static parent = new WeakMap();
|
|||
|
static contexts = new WeakMap();
|
|||
|
|
|||
|
constructor(width, height) {
|
|||
|
super(Canvas).alloc();
|
|||
|
Canvas.contexts.set(this, []);
|
|||
|
Object.assign(this, { width, height });
|
|||
|
}
|
|||
|
|
|||
|
getContext(kind) {
|
|||
|
return kind == "2d" ? Canvas.contexts.get(this)[0] || this.newPage() : null;
|
|||
|
}
|
|||
|
|
|||
|
get width() {
|
|||
|
return this.prop("width");
|
|||
|
}
|
|||
|
set width(w) {
|
|||
|
this.prop(
|
|||
|
"width",
|
|||
|
typeof w == "number" && !Number.isNaN(w) && w >= 0 ? w : 300
|
|||
|
);
|
|||
|
if (Canvas.contexts.get(this)[0])
|
|||
|
this.getContext("2d").ƒ("resetSize", core(this));
|
|||
|
}
|
|||
|
|
|||
|
get height() {
|
|||
|
return this.prop("height");
|
|||
|
}
|
|||
|
set height(h) {
|
|||
|
this.prop(
|
|||
|
"height",
|
|||
|
(h = typeof h == "number" && !Number.isNaN(h) && h >= 0 ? h : 150)
|
|||
|
);
|
|||
|
if (Canvas.contexts.get(this)[0])
|
|||
|
this.getContext("2d").ƒ("resetSize", core(this));
|
|||
|
}
|
|||
|
|
|||
|
newPage(width, height) {
|
|||
|
let ctx = new CanvasRenderingContext2D(core(this));
|
|||
|
Canvas.parent.set(ctx, this);
|
|||
|
Canvas.contexts.get(this).unshift(ctx);
|
|||
|
if (arguments.length == 2) {
|
|||
|
Object.assign(this, { width, height });
|
|||
|
}
|
|||
|
return ctx;
|
|||
|
}
|
|||
|
|
|||
|
get pages() {
|
|||
|
return Canvas.contexts
|
|||
|
.get(this)
|
|||
|
.slice()
|
|||
|
.reverse();
|
|||
|
}
|
|||
|
|
|||
|
get png() {
|
|||
|
return this.toBuffer("png");
|
|||
|
}
|
|||
|
get jpg() {
|
|||
|
return this.toBuffer("jpg");
|
|||
|
}
|
|||
|
get pdf() {
|
|||
|
return this.toBuffer("pdf");
|
|||
|
}
|
|||
|
get svg() {
|
|||
|
return this.toBuffer("svg");
|
|||
|
}
|
|||
|
|
|||
|
get async() {
|
|||
|
return this.prop("async");
|
|||
|
}
|
|||
|
set async(flag) {
|
|||
|
if (!flag) {
|
|||
|
process.emitWarning(
|
|||
|
"Use the saveAsSync, toBufferSync, and toDataURLSync methods instead of setting the Canvas `async` property to false",
|
|||
|
"DeprecationWarning"
|
|||
|
);
|
|||
|
}
|
|||
|
this.prop("async", flag);
|
|||
|
}
|
|||
|
|
|||
|
saveAs(filename, opts = {}) {
|
|||
|
if (!this.async) return this.saveAsSync(...arguments); // support while deprecated
|
|||
|
|
|||
|
opts = typeof opts == "number" ? { quality: opts } : opts;
|
|||
|
let {
|
|||
|
format,
|
|||
|
quality,
|
|||
|
pages,
|
|||
|
padding,
|
|||
|
pattern,
|
|||
|
density,
|
|||
|
outline,
|
|||
|
matte
|
|||
|
} = io.options(this.pages, { filename, ...opts }),
|
|||
|
args = [
|
|||
|
pages.map(core),
|
|||
|
pattern,
|
|||
|
padding,
|
|||
|
format,
|
|||
|
quality,
|
|||
|
density,
|
|||
|
outline,
|
|||
|
matte
|
|||
|
],
|
|||
|
worker = new EventEmitter();
|
|||
|
this.ƒ("save", (result, msg) => worker.emit(result, msg), ...args);
|
|||
|
return new Promise((res, rej) =>
|
|||
|
worker.once("ok", res).once("err", msg => rej(new Error(msg)))
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
saveAsSync(filename, opts = {}) {
|
|||
|
opts = typeof opts == "number" ? { quality: opts } : opts;
|
|||
|
let {
|
|||
|
format,
|
|||
|
quality,
|
|||
|
pages,
|
|||
|
padding,
|
|||
|
pattern,
|
|||
|
density,
|
|||
|
outline,
|
|||
|
matte
|
|||
|
} = io.options(this.pages, { filename, ...opts });
|
|||
|
this.ƒ(
|
|||
|
"saveSync",
|
|||
|
pages.map(core),
|
|||
|
pattern,
|
|||
|
padding,
|
|||
|
format,
|
|||
|
quality,
|
|||
|
density,
|
|||
|
outline,
|
|||
|
matte
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
toBuffer(extension = "png", opts = {}) {
|
|||
|
if (!this.async) return this.toBufferSync(...arguments); // support while deprecated
|
|||
|
|
|||
|
opts = typeof opts == "number" ? { quality: opts } : opts;
|
|||
|
let { format, quality, pages, density, outline, matte } = io.options(
|
|||
|
this.pages,
|
|||
|
{ extension, ...opts }
|
|||
|
),
|
|||
|
args = [pages.map(core), format, quality, density, outline, matte],
|
|||
|
worker = new EventEmitter();
|
|||
|
this.ƒ("toBuffer", (result, msg) => worker.emit(result, msg), ...args);
|
|||
|
return new Promise((res, rej) =>
|
|||
|
worker.once("ok", res).once("err", msg => rej(new Error(msg)))
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
toBufferSync(extension = "png", opts = {}) {
|
|||
|
opts = typeof opts == "number" ? { quality: opts } : opts;
|
|||
|
let { format, quality, pages, density, outline, matte } = io.options(
|
|||
|
this.pages,
|
|||
|
{ extension, ...opts }
|
|||
|
);
|
|||
|
return this.ƒ(
|
|||
|
"toBufferSync",
|
|||
|
pages.map(core),
|
|||
|
format,
|
|||
|
quality,
|
|||
|
density,
|
|||
|
outline,
|
|||
|
matte
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
toDataURL(extension = "png", opts = {}) {
|
|||
|
if (!this.async) return this.toDataURLSync(...arguments); // support while deprecated
|
|||
|
|
|||
|
opts = typeof opts == "number" ? { quality: opts } : opts;
|
|||
|
let { mime } = io.options(this.pages, { extension, ...opts }),
|
|||
|
buffer = this.toBuffer(extension, opts);
|
|||
|
return buffer.then(
|
|||
|
data => `data:${mime};base64,${data.toString("base64")}`
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
toDataURLSync(extension = "png", opts = {}) {
|
|||
|
opts = typeof opts == "number" ? { quality: opts } : opts;
|
|||
|
let { mime } = io.options(this.pages, { extension, ...opts }),
|
|||
|
buffer = this.toBufferSync(extension, opts);
|
|||
|
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
let { width, height, async, pages } = this;
|
|||
|
return `Canvas ${inspect({ width, height, async, pages }, options)}`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class CanvasGradient extends RustClass {
|
|||
|
constructor(style, ...coords) {
|
|||
|
super(CanvasGradient);
|
|||
|
style = (style || "").toLowerCase();
|
|||
|
if (["linear", "radial", "conic"].includes(style))
|
|||
|
this.init(style, ...coords);
|
|||
|
else
|
|||
|
throw new Error(
|
|||
|
`Function is not a constructor (use CanvasRenderingContext2D's "createConicGradient", "createLinearGradient", and "createRadialGradient" methods instead)`
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
addColorStop(offset, color) {
|
|||
|
if (offset >= 0 && offset <= 1) this.ƒ("addColorStop", offset, color);
|
|||
|
else throw new Error("Color stop offsets must be between 0.0 and 1.0");
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
return `CanvasGradient (${this.ƒ("repr")})`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class CanvasPattern extends RustClass {
|
|||
|
constructor(src, repeat) {
|
|||
|
super(CanvasPattern);
|
|||
|
if (src instanceof Image) {
|
|||
|
this.init("from_image", core(src), repeat);
|
|||
|
} else if (src instanceof Canvas) {
|
|||
|
let ctx = src.getContext("2d");
|
|||
|
this.init("from_canvas", core(ctx), repeat);
|
|||
|
} else {
|
|||
|
throw new Error("CanvasPatterns require a source Image or a Canvas");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
setTransform(matrix) {
|
|||
|
if (arguments.length > 1) matrix = [...arguments];
|
|||
|
this.ƒ("setTransform", toSkMatrix(matrix));
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
return `CanvasPattern (${this.ƒ("repr")})`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class CanvasTexture extends RustClass {
|
|||
|
constructor(spacing, { path, line, color, angle, offset = 0 } = {}) {
|
|||
|
super(CanvasTexture);
|
|||
|
let [x, y] =
|
|||
|
typeof offset == "number" ? [offset, offset] : offset.slice(0, 2);
|
|||
|
let [h, v] =
|
|||
|
typeof spacing == "number" ? [spacing, spacing] : spacing.slice(0, 2);
|
|||
|
path = core(path);
|
|||
|
line = line != null ? line : path ? 0 : 1;
|
|||
|
angle = angle != null ? angle : path ? 0 : -Math.PI / 4;
|
|||
|
this.alloc(path, color, line, angle, h, v, x, y);
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
return `CanvasTexture (${this.ƒ("repr")})`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class CanvasRenderingContext2D extends RustClass {
|
|||
|
constructor(canvas) {
|
|||
|
try {
|
|||
|
super(CanvasRenderingContext2D).alloc(canvas);
|
|||
|
} catch (e) {
|
|||
|
throw new TypeError(
|
|||
|
`Function is not a constructor (use Canvas's "getContext" method instead)`
|
|||
|
);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
get canvas() {
|
|||
|
return Canvas.parent.get(this);
|
|||
|
}
|
|||
|
|
|||
|
// -- grid state ------------------------------------------------------------
|
|||
|
save() {
|
|||
|
this.ƒ("save");
|
|||
|
}
|
|||
|
restore() {
|
|||
|
this.ƒ("restore");
|
|||
|
}
|
|||
|
|
|||
|
get currentTransform() {
|
|||
|
return fromSkMatrix(this.prop("currentTransform"));
|
|||
|
}
|
|||
|
set currentTransform(matrix) {
|
|||
|
this.prop("currentTransform", toSkMatrix(matrix));
|
|||
|
}
|
|||
|
|
|||
|
resetTransform() {
|
|||
|
this.ƒ("resetTransform");
|
|||
|
}
|
|||
|
getTransform() {
|
|||
|
return this.currentTransform;
|
|||
|
}
|
|||
|
setTransform(matrix) {
|
|||
|
this.currentTransform = arguments.length > 1 ? [...arguments] : matrix;
|
|||
|
}
|
|||
|
|
|||
|
transform(a, b, c, d, e, f) {
|
|||
|
this.ƒ("transform", ...arguments);
|
|||
|
}
|
|||
|
translate(x, y) {
|
|||
|
this.ƒ("translate", ...arguments);
|
|||
|
}
|
|||
|
scale(x, y) {
|
|||
|
this.ƒ("scale", ...arguments);
|
|||
|
}
|
|||
|
rotate(angle) {
|
|||
|
this.ƒ("rotate", ...arguments);
|
|||
|
}
|
|||
|
|
|||
|
createProjection(quad, basis) {
|
|||
|
return fromSkMatrix(
|
|||
|
this.ƒ("createProjection", [quad].flat(), [basis].flat())
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
// -- bézier paths ----------------------------------------------------------
|
|||
|
beginPath() {
|
|||
|
this.ƒ("beginPath");
|
|||
|
}
|
|||
|
rect(x, y, width, height) {
|
|||
|
this.ƒ("rect", ...arguments);
|
|||
|
}
|
|||
|
arc(x, y, radius, startAngle, endAngle, isCCW) {
|
|||
|
this.ƒ("arc", ...arguments);
|
|||
|
}
|
|||
|
ellipse(x, y, xRadius, yRadius, rotation, startAngle, endAngle, isCCW) {
|
|||
|
this.ƒ("ellipse", ...arguments);
|
|||
|
}
|
|||
|
moveTo(x, y) {
|
|||
|
this.ƒ("moveTo", ...arguments);
|
|||
|
}
|
|||
|
lineTo(x, y) {
|
|||
|
this.ƒ("lineTo", ...arguments);
|
|||
|
}
|
|||
|
arcTo(x1, y1, x2, y2, radius) {
|
|||
|
this.ƒ("arcTo", ...arguments);
|
|||
|
}
|
|||
|
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|||
|
this.ƒ("bezierCurveTo", ...arguments);
|
|||
|
}
|
|||
|
quadraticCurveTo(cpx, cpy, x, y) {
|
|||
|
this.ƒ("quadraticCurveTo", ...arguments);
|
|||
|
}
|
|||
|
conicCurveTo(cpx, cpy, x, y, weight) {
|
|||
|
this.ƒ("conicCurveTo", ...arguments);
|
|||
|
}
|
|||
|
closePath() {
|
|||
|
this.ƒ("closePath");
|
|||
|
}
|
|||
|
isPointInPath(x, y) {
|
|||
|
return this.ƒ("isPointInPath", ...arguments);
|
|||
|
}
|
|||
|
isPointInStroke(x, y) {
|
|||
|
return this.ƒ("isPointInStroke", ...arguments);
|
|||
|
}
|
|||
|
|
|||
|
// -- using paths -----------------------------------------------------------
|
|||
|
fill(path, rule) {
|
|||
|
if (path instanceof Path2D) this.ƒ("fill", core(path), rule);
|
|||
|
else this.ƒ("fill", path); // 'path' is the optional winding-rule
|
|||
|
}
|
|||
|
|
|||
|
stroke(path, rule) {
|
|||
|
if (path instanceof Path2D) this.ƒ("stroke", core(path), rule);
|
|||
|
else this.ƒ("stroke", path); // 'path' is the optional winding-rule
|
|||
|
}
|
|||
|
|
|||
|
clip(path, rule) {
|
|||
|
if (path instanceof Path2D) this.ƒ("clip", core(path), rule);
|
|||
|
else this.ƒ("clip", path); // 'path' is the optional winding-rule
|
|||
|
}
|
|||
|
|
|||
|
// -- shaders ---------------------------------------------------------------
|
|||
|
createPattern(image, repetition) {
|
|||
|
return new CanvasPattern(...arguments);
|
|||
|
}
|
|||
|
createLinearGradient(x0, y0, x1, y1) {
|
|||
|
return new CanvasGradient("Linear", ...arguments);
|
|||
|
}
|
|||
|
createRadialGradient(x0, y0, r0, x1, y1, r1) {
|
|||
|
return new CanvasGradient("Radial", ...arguments);
|
|||
|
}
|
|||
|
createConicGradient(startAngle, x, y) {
|
|||
|
return new CanvasGradient("Conic", ...arguments);
|
|||
|
}
|
|||
|
|
|||
|
createTexture(spacing, options) {
|
|||
|
return new CanvasTexture(spacing, options);
|
|||
|
}
|
|||
|
|
|||
|
// -- fill & stroke ---------------------------------------------------------
|
|||
|
fillRect(x, y, width, height) {
|
|||
|
this.ƒ("fillRect", ...arguments);
|
|||
|
}
|
|||
|
strokeRect(x, y, width, height) {
|
|||
|
this.ƒ("strokeRect", ...arguments);
|
|||
|
}
|
|||
|
clearRect(x, y, width, height) {
|
|||
|
this.ƒ("clearRect", ...arguments);
|
|||
|
}
|
|||
|
|
|||
|
set fillStyle(style) {
|
|||
|
let isShader =
|
|||
|
style instanceof CanvasPattern ||
|
|||
|
style instanceof CanvasGradient ||
|
|||
|
style instanceof CanvasTexture,
|
|||
|
[ref, val] = isShader ? [style, core(style)] : [null, style];
|
|||
|
this.ref("fill", ref);
|
|||
|
this.prop("fillStyle", val);
|
|||
|
}
|
|||
|
|
|||
|
get fillStyle() {
|
|||
|
let style = this.prop("fillStyle");
|
|||
|
return style === null ? this.ref("fill") : style;
|
|||
|
}
|
|||
|
|
|||
|
set strokeStyle(style) {
|
|||
|
let isShader =
|
|||
|
style instanceof CanvasPattern ||
|
|||
|
style instanceof CanvasGradient ||
|
|||
|
style instanceof CanvasTexture,
|
|||
|
[ref, val] = isShader ? [style, core(style)] : [null, style];
|
|||
|
this.ref("stroke", ref);
|
|||
|
this.prop("strokeStyle", val);
|
|||
|
}
|
|||
|
|
|||
|
get strokeStyle() {
|
|||
|
let style = this.prop("strokeStyle");
|
|||
|
return style === null ? this.ref("stroke") : style;
|
|||
|
}
|
|||
|
|
|||
|
// -- line style ------------------------------------------------------------
|
|||
|
getLineDash() {
|
|||
|
return this.ƒ("getLineDash");
|
|||
|
}
|
|||
|
setLineDash(segments) {
|
|||
|
this.ƒ("setLineDash", segments);
|
|||
|
}
|
|||
|
get lineCap() {
|
|||
|
return this.prop("lineCap");
|
|||
|
}
|
|||
|
set lineCap(style) {
|
|||
|
this.prop("lineCap", style);
|
|||
|
}
|
|||
|
get lineDashFit() {
|
|||
|
return this.prop("lineDashFit");
|
|||
|
}
|
|||
|
set lineDashFit(style) {
|
|||
|
this.prop("lineDashFit", style);
|
|||
|
}
|
|||
|
get lineDashMarker() {
|
|||
|
return wrap(Path2D, this.prop("lineDashMarker"));
|
|||
|
}
|
|||
|
set lineDashMarker(path) {
|
|||
|
this.prop("lineDashMarker", path instanceof Path2D ? core(path) : path);
|
|||
|
}
|
|||
|
get lineDashOffset() {
|
|||
|
return this.prop("lineDashOffset");
|
|||
|
}
|
|||
|
set lineDashOffset(offset) {
|
|||
|
this.prop("lineDashOffset", offset);
|
|||
|
}
|
|||
|
get lineJoin() {
|
|||
|
return this.prop("lineJoin");
|
|||
|
}
|
|||
|
set lineJoin(style) {
|
|||
|
this.prop("lineJoin", style);
|
|||
|
}
|
|||
|
get lineWidth() {
|
|||
|
return this.prop("lineWidth");
|
|||
|
}
|
|||
|
set lineWidth(width) {
|
|||
|
this.prop("lineWidth", width);
|
|||
|
}
|
|||
|
get miterLimit() {
|
|||
|
return this.prop("miterLimit");
|
|||
|
}
|
|||
|
set miterLimit(limit) {
|
|||
|
this.prop("miterLimit", limit);
|
|||
|
}
|
|||
|
|
|||
|
// -- imagery ---------------------------------------------------------------
|
|||
|
get imageSmoothingEnabled() {
|
|||
|
return this.prop("imageSmoothingEnabled");
|
|||
|
}
|
|||
|
set imageSmoothingEnabled(flag) {
|
|||
|
this.prop("imageSmoothingEnabled", !!flag);
|
|||
|
}
|
|||
|
get imageSmoothingQuality() {
|
|||
|
return this.prop("imageSmoothingQuality");
|
|||
|
}
|
|||
|
set imageSmoothingQuality(level) {
|
|||
|
this.prop("imageSmoothingQuality", level);
|
|||
|
}
|
|||
|
putImageData(imageData, ...coords) {
|
|||
|
this.ƒ("putImageData", imageData, ...coords);
|
|||
|
}
|
|||
|
createImageData(width, height) {
|
|||
|
return new ImageData(width, height);
|
|||
|
}
|
|||
|
|
|||
|
getImageData(x, y, width, height) {
|
|||
|
let w = Math.floor(width),
|
|||
|
h = Math.floor(height),
|
|||
|
buffer = this.ƒ("getImageData", x, y, w, h);
|
|||
|
return new ImageData(buffer, w, h);
|
|||
|
}
|
|||
|
|
|||
|
drawImage(image, ...coords) {
|
|||
|
if (image instanceof Canvas) {
|
|||
|
this.ƒ("drawImage", core(image.getContext("2d")), ...coords);
|
|||
|
} else if (image instanceof Image) {
|
|||
|
this.ƒ("drawImage", core(image), ...coords);
|
|||
|
} else {
|
|||
|
throw new Error("Expected an Image or a Canvas argument");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
drawCanvas(image, ...coords) {
|
|||
|
if (image instanceof Canvas) {
|
|||
|
this.ƒ("drawCanvas", core(image.getContext("2d")), ...coords);
|
|||
|
} else {
|
|||
|
this.drawImage(image, ...coords);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// -- typography ------------------------------------------------------------
|
|||
|
get font() {
|
|||
|
return this.prop("font");
|
|||
|
}
|
|||
|
set font(str) {
|
|||
|
this.prop("font", css.font(str));
|
|||
|
}
|
|||
|
get textAlign() {
|
|||
|
return this.prop("textAlign");
|
|||
|
}
|
|||
|
set textAlign(mode) {
|
|||
|
this.prop("textAlign", mode);
|
|||
|
}
|
|||
|
get textBaseline() {
|
|||
|
return this.prop("textBaseline");
|
|||
|
}
|
|||
|
set textBaseline(mode) {
|
|||
|
this.prop("textBaseline", mode);
|
|||
|
}
|
|||
|
get direction() {
|
|||
|
return this.prop("direction");
|
|||
|
}
|
|||
|
set direction(mode) {
|
|||
|
this.prop("direction", mode);
|
|||
|
}
|
|||
|
|
|||
|
measureText(text, maxWidth) {
|
|||
|
text = this.textWrap ? text : text + "\u200b"; // include trailing whitespace by default
|
|||
|
let [metrics, ...lines] = this.ƒ("measureText", toString(text), maxWidth);
|
|||
|
return new TextMetrics(metrics, lines);
|
|||
|
}
|
|||
|
|
|||
|
fillText(text, x, y, maxWidth) {
|
|||
|
this.ƒ("fillText", toString(text), x, y, maxWidth);
|
|||
|
}
|
|||
|
|
|||
|
strokeText(text, x, y, maxWidth) {
|
|||
|
this.ƒ("strokeText", toString(text), x, y, maxWidth);
|
|||
|
}
|
|||
|
|
|||
|
outlineText(text) {
|
|||
|
let path = this.ƒ("outlineText", toString(text));
|
|||
|
return path ? wrap(Path2D, path) : null;
|
|||
|
}
|
|||
|
|
|||
|
// -- non-standard typography extensions --------------------------------------------
|
|||
|
get fontVariant() {
|
|||
|
return this.prop("fontVariant");
|
|||
|
}
|
|||
|
set fontVariant(str) {
|
|||
|
this.prop("fontVariant", css.variant(str));
|
|||
|
}
|
|||
|
get textTracking() {
|
|||
|
return this.prop("textTracking");
|
|||
|
}
|
|||
|
set textTracking(ems) {
|
|||
|
this.prop("textTracking", ems);
|
|||
|
}
|
|||
|
get textWrap() {
|
|||
|
return this.prop("textWrap");
|
|||
|
}
|
|||
|
set textWrap(flag) {
|
|||
|
this.prop("textWrap", !!flag);
|
|||
|
}
|
|||
|
|
|||
|
// -- effects ---------------------------------------------------------------
|
|||
|
get globalCompositeOperation() {
|
|||
|
return this.prop("globalCompositeOperation");
|
|||
|
}
|
|||
|
set globalCompositeOperation(blend) {
|
|||
|
this.prop("globalCompositeOperation", blend);
|
|||
|
}
|
|||
|
get globalAlpha() {
|
|||
|
return this.prop("globalAlpha");
|
|||
|
}
|
|||
|
set globalAlpha(alpha) {
|
|||
|
this.prop("globalAlpha", alpha);
|
|||
|
}
|
|||
|
get shadowBlur() {
|
|||
|
return this.prop("shadowBlur");
|
|||
|
}
|
|||
|
set shadowBlur(level) {
|
|||
|
this.prop("shadowBlur", level);
|
|||
|
}
|
|||
|
get shadowColor() {
|
|||
|
return this.prop("shadowColor");
|
|||
|
}
|
|||
|
set shadowColor(color) {
|
|||
|
this.prop("shadowColor", color);
|
|||
|
}
|
|||
|
get shadowOffsetX() {
|
|||
|
return this.prop("shadowOffsetX");
|
|||
|
}
|
|||
|
set shadowOffsetX(x) {
|
|||
|
this.prop("shadowOffsetX", x);
|
|||
|
}
|
|||
|
get shadowOffsetY() {
|
|||
|
return this.prop("shadowOffsetY");
|
|||
|
}
|
|||
|
set shadowOffsetY(y) {
|
|||
|
this.prop("shadowOffsetY", y);
|
|||
|
}
|
|||
|
get filter() {
|
|||
|
return this.prop("filter");
|
|||
|
}
|
|||
|
set filter(str) {
|
|||
|
this.prop("filter", css.filter(str));
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
let props = [
|
|||
|
"canvas",
|
|||
|
"currentTransform",
|
|||
|
"fillStyle",
|
|||
|
"strokeStyle",
|
|||
|
"font",
|
|||
|
"fontVariant",
|
|||
|
"direction",
|
|||
|
"textAlign",
|
|||
|
"textBaseline",
|
|||
|
"textTracking",
|
|||
|
"textWrap",
|
|||
|
"globalAlpha",
|
|||
|
"globalCompositeOperation",
|
|||
|
"imageSmoothingEnabled",
|
|||
|
"imageSmoothingQuality",
|
|||
|
"filter",
|
|||
|
"shadowBlur",
|
|||
|
"shadowColor",
|
|||
|
"shadowOffsetX",
|
|||
|
"shadowOffsetY",
|
|||
|
"lineCap",
|
|||
|
"lineDashOffset",
|
|||
|
"lineJoin",
|
|||
|
"lineWidth",
|
|||
|
"miterLimit"
|
|||
|
];
|
|||
|
let info = {};
|
|||
|
if (depth > 0) {
|
|||
|
for (var prop of props) {
|
|||
|
try {
|
|||
|
info[prop] = this[prop];
|
|||
|
} catch {
|
|||
|
info[prop] = undefined;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return `CanvasRenderingContext2D ${inspect(info, options)}`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const _expand = paths =>
|
|||
|
[paths]
|
|||
|
.flat(2)
|
|||
|
.map(pth => (hasMagic(pth) ? glob(pth) : pth))
|
|||
|
.flat();
|
|||
|
|
|||
|
class FontLibrary extends RustClass {
|
|||
|
constructor() {
|
|||
|
super(FontLibrary);
|
|||
|
}
|
|||
|
|
|||
|
get families() {
|
|||
|
return this.prop("families");
|
|||
|
}
|
|||
|
|
|||
|
has(familyName) {
|
|||
|
return this.ƒ("has", familyName);
|
|||
|
}
|
|||
|
|
|||
|
family(name) {
|
|||
|
return this.ƒ("family", name);
|
|||
|
}
|
|||
|
|
|||
|
use(...args) {
|
|||
|
let sig = signature(args);
|
|||
|
if (sig == "o") {
|
|||
|
let results = {};
|
|||
|
for (let [alias, paths] of Object.entries(args.shift())) {
|
|||
|
results[alias] = this.ƒ("addFamily", alias, _expand(paths));
|
|||
|
}
|
|||
|
return results;
|
|||
|
} else if (sig.match(/^s?[as]$/)) {
|
|||
|
let fonts = _expand(args.pop());
|
|||
|
let alias = args.shift();
|
|||
|
return this.ƒ("addFamily", alias, fonts);
|
|||
|
} else {
|
|||
|
throw new Error(
|
|||
|
"Expected an array of file paths or an object mapping family names to font files"
|
|||
|
);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class Image extends RustClass {
|
|||
|
constructor() {
|
|||
|
super(Image).alloc();
|
|||
|
}
|
|||
|
|
|||
|
get complete() {
|
|||
|
return this.prop("complete");
|
|||
|
}
|
|||
|
get height() {
|
|||
|
return this.prop("height");
|
|||
|
}
|
|||
|
get width() {
|
|||
|
return this.prop("width");
|
|||
|
}
|
|||
|
|
|||
|
get src() {
|
|||
|
return this.prop("src");
|
|||
|
}
|
|||
|
set src(src) {
|
|||
|
var noop = () => {},
|
|||
|
onload = img => fetch.emit("ok", img),
|
|||
|
onerror = err => fetch.emit("err", err),
|
|||
|
passthrough = fn => arg => {
|
|||
|
(fn || noop)(arg);
|
|||
|
delete this._fetch;
|
|||
|
},
|
|||
|
data;
|
|||
|
|
|||
|
if (this._fetch) this._fetch.removeAllListeners();
|
|||
|
let fetch = (this._fetch = new EventEmitter()
|
|||
|
.once("ok", passthrough(this.onload))
|
|||
|
.once("err", passthrough(this.onerror)));
|
|||
|
|
|||
|
if (Buffer.isBuffer(src)) {
|
|||
|
[data, src] = [src, ""];
|
|||
|
} else if (typeof src != "string") {
|
|||
|
return;
|
|||
|
} else if (/^\s*data:/.test(src)) {
|
|||
|
// data URI
|
|||
|
let split = src.indexOf(","),
|
|||
|
enc = src.lastIndexOf("base64", split) !== -1 ? "base64" : "utf8",
|
|||
|
content = src.slice(split + 1);
|
|||
|
data = Buffer.from(content, enc);
|
|||
|
} else if (/^\s*https?:\/\//.test(src)) {
|
|||
|
// remote URL
|
|||
|
get.concat(src, (err, res, data) => {
|
|||
|
let code = (res || {}).statusCode;
|
|||
|
if (err) onerror(err);
|
|||
|
else if (code < 200 || code >= 300) {
|
|||
|
onerror(
|
|||
|
new Error(`Failed to load image from "${src}" (error ${code})`)
|
|||
|
);
|
|||
|
} else {
|
|||
|
if (this.prop("data", data)) onload(this);
|
|||
|
else onerror(new Error("Could not decode image data"));
|
|||
|
}
|
|||
|
});
|
|||
|
} else {
|
|||
|
// local file path
|
|||
|
data = fs.readFileSync(src);
|
|||
|
}
|
|||
|
|
|||
|
this.prop("src", src);
|
|||
|
if (data) {
|
|||
|
if (this.prop("data", data)) onload(this);
|
|||
|
else onerror(new Error("Could not decode image data"));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
decode() {
|
|||
|
return this._fetch
|
|||
|
? new Promise((res, rej) => this._fetch.once("ok", res).once("err", rej))
|
|||
|
: this.complete
|
|||
|
? Promise.resolve(this)
|
|||
|
: Promise.reject(new Error("Missing Source URL"));
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
let { width, height, complete, src } = this;
|
|||
|
options.maxStringLength = src.match(/^data:/) ? 128 : Infinity;
|
|||
|
return `Image ${inspect({ width, height, complete, src }, options)}`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class ImageData {
|
|||
|
constructor(...args) {
|
|||
|
if (args[0] instanceof ImageData) {
|
|||
|
var { data, width, height } = args[0];
|
|||
|
} else if (
|
|||
|
args[0] instanceof Uint8ClampedArray ||
|
|||
|
args[0] instanceof Buffer
|
|||
|
) {
|
|||
|
var [data, width, height] = args;
|
|||
|
height = height || data.length / width / 4;
|
|||
|
if (data.length / 4 != width * height) {
|
|||
|
throw new Error("ImageData dimensions must match buffer length");
|
|||
|
}
|
|||
|
} else {
|
|||
|
var [width, height] = args;
|
|||
|
}
|
|||
|
|
|||
|
if (
|
|||
|
!Number.isInteger(width) ||
|
|||
|
!Number.isInteger(height) ||
|
|||
|
width < 0 ||
|
|||
|
height < 0
|
|||
|
) {
|
|||
|
throw new Error("ImageData dimensions must be positive integers");
|
|||
|
}
|
|||
|
|
|||
|
readOnly(this, "width", width);
|
|||
|
readOnly(this, "height", height);
|
|||
|
readOnly(
|
|||
|
this,
|
|||
|
"data",
|
|||
|
new Uint8ClampedArray((data && data.buffer) || width * height * 4)
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
let { width, height, data } = this;
|
|||
|
return `ImageData ${inspect({ width, height, data }, options)}`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class Path2D extends RustClass {
|
|||
|
static op(operation, path, other) {
|
|||
|
return wrap(Path2D, path.ƒ("op", core(other), operation));
|
|||
|
}
|
|||
|
|
|||
|
static interpolate(path, other, weight) {
|
|||
|
return wrap(Path2D, path.ƒ("interpolate", core(other), weight));
|
|||
|
}
|
|||
|
|
|||
|
static effect(effect, path, ...args) {
|
|||
|
return wrap(Path2D, path.ƒ(effect, ...args));
|
|||
|
}
|
|||
|
|
|||
|
constructor(source) {
|
|||
|
super(Path2D);
|
|||
|
if (source instanceof Path2D) this.init("from_path", core(source));
|
|||
|
else if (typeof source == "string") this.init("from_svg", source);
|
|||
|
else this.alloc();
|
|||
|
}
|
|||
|
|
|||
|
// dimensions & contents
|
|||
|
get bounds() {
|
|||
|
return this.ƒ("bounds");
|
|||
|
}
|
|||
|
get edges() {
|
|||
|
return this.ƒ("edges");
|
|||
|
}
|
|||
|
get d() {
|
|||
|
return this.prop("d");
|
|||
|
}
|
|||
|
set d(svg) {
|
|||
|
return this.prop("d", svg);
|
|||
|
}
|
|||
|
contains(x, y) {
|
|||
|
return this.ƒ("contains", x, y);
|
|||
|
}
|
|||
|
|
|||
|
points(step = 1) {
|
|||
|
return this.jitter(step, 0)
|
|||
|
.edges.map(([verb, ...pts]) => pts.slice(-2))
|
|||
|
.filter(pt => pt.length);
|
|||
|
}
|
|||
|
|
|||
|
// concatenation
|
|||
|
addPath(path, matrix) {
|
|||
|
if (!(path instanceof Path2D)) throw new Error("Expected a Path2D object");
|
|||
|
if (matrix) matrix = toSkMatrix(matrix);
|
|||
|
this.ƒ("addPath", core(path), matrix);
|
|||
|
}
|
|||
|
|
|||
|
// line segments
|
|||
|
moveTo(x, y) {
|
|||
|
this.ƒ("moveTo", ...arguments);
|
|||
|
}
|
|||
|
lineTo(x, y) {
|
|||
|
this.ƒ("lineTo", ...arguments);
|
|||
|
}
|
|||
|
closePath() {
|
|||
|
this.ƒ("closePath");
|
|||
|
}
|
|||
|
arcTo(x1, y1, x2, y2, radius) {
|
|||
|
this.ƒ("arcTo", ...arguments);
|
|||
|
}
|
|||
|
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|||
|
this.ƒ("bezierCurveTo", ...arguments);
|
|||
|
}
|
|||
|
quadraticCurveTo(cpx, cpy, x, y) {
|
|||
|
this.ƒ("quadraticCurveTo", ...arguments);
|
|||
|
}
|
|||
|
conicCurveTo(cpx, cpy, x, y, weight) {
|
|||
|
this.ƒ("conicCurveTo", ...arguments);
|
|||
|
}
|
|||
|
|
|||
|
// shape primitives
|
|||
|
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, isCCW) {
|
|||
|
this.ƒ("ellipse", ...arguments);
|
|||
|
}
|
|||
|
rect(x, y, width, height) {
|
|||
|
this.ƒ("rect", ...arguments);
|
|||
|
}
|
|||
|
arc(x, y, radius, startAngle, endAngle) {
|
|||
|
this.ƒ("arc", ...arguments);
|
|||
|
}
|
|||
|
|
|||
|
// tween similar paths
|
|||
|
interpolate(path, weight) {
|
|||
|
return Path2D.interpolate(this, path, weight);
|
|||
|
}
|
|||
|
|
|||
|
// boolean operations
|
|||
|
complement(path) {
|
|||
|
return Path2D.op("complement", this, path);
|
|||
|
}
|
|||
|
difference(path) {
|
|||
|
return Path2D.op("difference", this, path);
|
|||
|
}
|
|||
|
intersect(path) {
|
|||
|
return Path2D.op("intersect", this, path);
|
|||
|
}
|
|||
|
union(path) {
|
|||
|
return Path2D.op("union", this, path);
|
|||
|
}
|
|||
|
xor(path) {
|
|||
|
return Path2D.op("xor", this, path);
|
|||
|
}
|
|||
|
|
|||
|
// path effects
|
|||
|
jitter(len, amt, seed) {
|
|||
|
return Path2D.effect("jitter", this, ...arguments);
|
|||
|
}
|
|||
|
simplify(rule) {
|
|||
|
return Path2D.effect("simplify", this, rule);
|
|||
|
}
|
|||
|
unwind() {
|
|||
|
return Path2D.effect("unwind", this);
|
|||
|
}
|
|||
|
round(radius) {
|
|||
|
return Path2D.effect("round", this, radius);
|
|||
|
}
|
|||
|
offset(dx, dy) {
|
|||
|
return Path2D.effect("offset", this, dx, dy);
|
|||
|
}
|
|||
|
|
|||
|
transform(matrix) {
|
|||
|
let terms = arguments.length > 1 ? [...arguments] : matrix;
|
|||
|
return Path2D.effect("transform", this, toSkMatrix(terms));
|
|||
|
}
|
|||
|
|
|||
|
trim(...rng) {
|
|||
|
if (typeof rng[1] != "number") {
|
|||
|
if (rng[0] > 0) rng.unshift(0);
|
|||
|
else if (rng[0] < 0) rng.splice(1, 0, 1);
|
|||
|
}
|
|||
|
if (rng[0] < 0) rng[0] = Math.max(-1, rng[0]) + 1;
|
|||
|
if (rng[1] < 0) rng[1] = Math.max(-1, rng[1]) + 1;
|
|||
|
return Path2D.effect("trim", this, ...rng);
|
|||
|
}
|
|||
|
|
|||
|
[REPR](depth, options) {
|
|||
|
let { d, bounds, edges } = this;
|
|||
|
return `Path2D ${inspect({ d, bounds, edges }, options)}`;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class TextMetrics {
|
|||
|
constructor(
|
|||
|
[
|
|||
|
width,
|
|||
|
left,
|
|||
|
right,
|
|||
|
ascent,
|
|||
|
descent,
|
|||
|
fontAscent,
|
|||
|
fontDescent,
|
|||
|
emAscent,
|
|||
|
emDescent,
|
|||
|
hanging,
|
|||
|
alphabetic,
|
|||
|
ideographic
|
|||
|
],
|
|||
|
lines
|
|||
|
) {
|
|||
|
readOnly(this, "width", width);
|
|||
|
readOnly(this, "actualBoundingBoxLeft", left);
|
|||
|
readOnly(this, "actualBoundingBoxRight", right);
|
|||
|
readOnly(this, "actualBoundingBoxAscent", ascent);
|
|||
|
readOnly(this, "actualBoundingBoxDescent", descent);
|
|||
|
readOnly(this, "fontBoundingBoxAscent", fontAscent);
|
|||
|
readOnly(this, "fontBoundingBoxDescent", fontDescent);
|
|||
|
readOnly(this, "emHeightAscent", emAscent);
|
|||
|
readOnly(this, "emHeightDescent", emDescent);
|
|||
|
readOnly(this, "hangingBaseline", hanging);
|
|||
|
readOnly(this, "alphabeticBaseline", alphabetic);
|
|||
|
readOnly(this, "ideographicBaseline", ideographic);
|
|||
|
readOnly(
|
|||
|
this,
|
|||
|
"lines",
|
|||
|
lines.map(([x, y, width, height, baseline, startIndex, endIndex]) => ({
|
|||
|
x,
|
|||
|
y,
|
|||
|
width,
|
|||
|
height,
|
|||
|
baseline,
|
|||
|
startIndex,
|
|||
|
endIndex
|
|||
|
}))
|
|||
|
);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
const loadImage = src => Object.assign(new Image(), { src }).decode();
|
|||
|
|
|||
|
// module.exports = {
|
|||
|
// Canvas,
|
|||
|
// CanvasGradient,
|
|||
|
// CanvasPattern,
|
|||
|
// CanvasRenderingContext2D,
|
|||
|
// CanvasTexture,
|
|||
|
// TextMetrics,
|
|||
|
// Image,
|
|||
|
// ImageData,
|
|||
|
// Path2D,
|
|||
|
// loadImage,
|
|||
|
// ...geometry,
|
|||
|
// FontLibrary: new FontLibrary()
|
|||
|
// };
|
|||
|
const obj = {
|
|||
|
Canvas,
|
|||
|
CanvasGradient,
|
|||
|
CanvasPattern,
|
|||
|
CanvasRenderingContext2D,
|
|||
|
CanvasTexture,
|
|||
|
TextMetrics,
|
|||
|
Image,
|
|||
|
ImageData,
|
|||
|
Path2D,
|
|||
|
loadImage,
|
|||
|
...geometry,
|
|||
|
FontLibrary: new FontLibrary()
|
|||
|
};
|
|||
|
export default obj;
|