1209 lines
30 KiB
JavaScript
Raw Normal View History

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