1209 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 DOMMatrixs 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;