[PathKit] Write more complete docs and clean up API to be consistent

Breaking Changes (should be minor, as it's mostly just things
for testing):
 - PathKit.ApplyPathOp should have returned a new SkPath, but didn't.
It now does and is named "MakeFromOp", which makes the convention of
"Have 'make' in name, needs delete" more consistent.
 - PathKit.FromCmds(arr) now only needs to take the JS Array and
will handle the TypedArrays under the hood. If clients want to deal
with TypedArrays themselves, they can use _FromCmds(ptr, len) directly.
 - PathKit.MakeLTRBRect is now just PathKit.LTRBRect. The thing
returned is a normal JS Object and doesn't need delete().

As per custom with v0 apps, we are updating the minor version
to v0.3.0 to account for breaking changes.


Docs-Preview: https://skia.org/?cl=147960
Bug: skia:8216
Change-Id: Ia3626e69f3e97698fc62a6aee876af005e29ffca
Reviewed-on: https://skia-review.googlesource.com/147960
Reviewed-by: Mike Reed <reed@google.com>
Reviewed-by: Heather Miller <hcm@google.com>
This commit is contained in:
Kevin Lubick 2018-08-24 10:44:16 -04:00
parent 1118cfdbdc
commit d993648fa4
13 changed files with 898 additions and 99 deletions

View File

@ -13,7 +13,7 @@ npm: npm-test npm-debug
publish:
cd npm-wasm; npm publish
cd ../npm-asmjs; npm publish
cd npm-asmjs; npm publish
update-major:
cd npm-wasm; npm version major
@ -22,12 +22,12 @@ update-major:
update-minor:
cd npm-wasm; npm version minor
cd ../npm-asmjs; npm version minor
cd npm-asmjs; npm version minor
echo "Don't forget to publish."
update-patch:
cd npm-wasm; npm version patch
cd ../npm-asmjs; npm version patch
cd npm-asmjs; npm version patch
echo "Don't forget to publish."
# Build the library and run the tests. If developing locally, test-continuous is better

View File

@ -115,13 +115,13 @@
return null;
};
PathKit.SkPath.prototype.quadraticCurveTo = function(x1, y1, x2, y2) {
this._quadTo(x1, y1, x2, y2);
PathKit.SkPath.prototype.quadraticCurveTo = function(cpx, cpy, x, y) {
this._quadTo(cpx, cpy, x, y);
return this;
};
PathKit.SkPath.prototype.quadTo = function(x1, y1, x2, y2) {
this._quadTo(x1, y1, x2, y2);
PathKit.SkPath.prototype.quadTo = function(cpx, cpy, x, y) {
this._quadTo(cpx, cpy, x, y);
return this;
};

View File

@ -24,6 +24,9 @@ var PathKit = {
SkBits2FloatUnsigned: function(num) {},
_malloc: function(size) {},
onRuntimeInitialized: function() {},
_FromCmds: function(ptr, size) {},
loadCmdsTypedArray: function(arr) {},
FromCmds: function(arr) {},
HEAPF32: {},
@ -40,7 +43,7 @@ var PathKit = {
_lineTo: function(x1, y1) {},
_moveTo: function(x1, y1) {},
_op: function(otherPath, op) {},
_quadTo: function(x1, y1, x2, y2) {},
_quadTo: function(cpx, cpy, x, y) {},
_rect: function(x, y, w, h) {},
_simplify: function() {},
_stroke: function(opts) {},

View File

@ -7,9 +7,9 @@
var Float32ArrayCache = {};
// Takes a 2D array of commands and puts them into the WASM heap
// as a 1D array. This allowing them to referenced from the C++ code.
// as a 1D array. This allows them to referenced from the C++ code.
// Returns a 2 element array, with the first item being essentially a
// pointer to the array and the second item being the lengh of
// pointer to the array and the second item being the length of
// the new 1D array.
//
// Example usage:
@ -26,7 +26,7 @@
//
// If arguments at index 1... in each cmd row are strings, they will be
// parsed as hex, and then converted to floats using SkBits2FloatUnsigned
PathKit['loadCmdsTypedArray'] = function(arr) {
PathKit.loadCmdsTypedArray = function(arr) {
var len = 0;
for (var r = 0; r < arr.length; r++) {
len += arr[r].length;
@ -57,5 +57,13 @@
PathKit.HEAPF32.set(ta, ptr / ta.BYTES_PER_ELEMENT);
return [ptr, len];
}
// Experimentation has shown that using TypedArrays to pass arrays from
// JS to C++ is faster than passing the JS Arrays across.
// See above for example of cmds.
PathKit.FromCmds = function(cmds) {
var ptrLen = PathKit.loadCmdsTypedArray(cmds);
return PathKit._FromCmds(ptrLen[0], ptrLen[1]);
}
}(Module)); // When this file is loaded in, the high level object is "Module";

View File

@ -37,6 +37,7 @@
<canvas class=big id=canvas8></canvas>
<canvas class=big id=canvas9></canvas>
<canvas class=big id=canvas10></canvas>
<canvas class=big id=canvas11></canvas>
<canvas class=big id=canvasTransform></canvas>
<h2> Supports fill-rules of nonzero and evenodd </h2>
@ -193,58 +194,74 @@
// no-op
(path) => path,
// dash
(path) => path.dash(10, 3, 0),
(path, counter) => path.dash(10, 3, counter/5),
// trim (takes optional 3rd param for returning the trimmed part
// or the complement)
(path) => path.trim(0.25, 0.8, false),
(path, counter) => path.trim((counter/100) % 1, 0.8, false),
// simplify
(path) => path.simplify(),
// stroke
(path) => path.stroke({
width: 15,
(path, counter) => path.stroke({
width: 10 * (Math.sin(counter/30) + 1),
join: PathKit.StrokeJoin.BEVEL,
cap: PathKit.StrokeCap.BUTT,
miter_limit: 1,
}),
// "offset effect", that is, making a border around the shape.
(path) => {
(path, counter) => {
let orig = path.copy();
path.stroke({
width: 10,
width: 10 + (counter / 4) % 50,
join: PathKit.StrokeJoin.ROUND,
cap: PathKit.StrokeCap.SQUARE,
})
.op(orig, PathKit.PathOp.DIFFERENCE);
orig.delete();
},
(path, counter) => {
let simplified = path.simplify().copy();
path.stroke({
width: 2 + (counter / 2) % 100,
join: PathKit.StrokeJoin.BEVEL,
cap: PathKit.StrokeCap.BUTT,
})
.op(simplified, PathKit.PathOp.REVERSE_DIFFERENCE);
simplified.delete();
}
];
let names = ["(plain)", "Dash", "Trim", "Simplify", "Stroke", "Offset"];
let names = ["(plain)", "Dash", "Trim", "Simplify", "Stroke", "Grow", "Shrink"];
for (let i = 0; i < effects.length; i++) {
let path = PathKit.NewPath();
drawStar(path);
let counter = 0;
function frame() {
counter++;
for (let i = 0; i < effects.length; i++) {
let path = PathKit.NewPath();
drawStar(path);
// The transforms apply directly to the path.
effects[i](path);
// The transforms apply directly to the path.
effects[i](path, counter);
let ctx = document.getElementById(`canvas${i+5}`).getContext('2d');
setCanvasSize(ctx, 300, 300);
ctx.strokeStyle = '#3c597a';
ctx.fillStyle = '#3c597a';
if (i === 4 || i === 5) {
ctx.fill(path.toPath2D(), path.getFillTypeString());
} else {
ctx.stroke(path.toPath2D());
let ctx = document.getElementById(`canvas${i+5}`).getContext('2d');
setCanvasSize(ctx, 300, 300);
ctx.strokeStyle = '#3c597a';
ctx.fillStyle = '#3c597a';
if (i >=4 ) {
ctx.fill(path.toPath2D(), path.getFillTypeString());
} else {
ctx.stroke(path.toPath2D());
}
ctx.font = '42px monospace';
let x = 150-ctx.measureText(names[i]).width/2;
ctx.strokeText(names[i], x, 290);
path.delete();
}
ctx.font = '42px monospace';
let x = 150-ctx.measureText(names[i]).width/2;
ctx.strokeText(names[i], x, 290);
path.delete();
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame(frame);
}
function MatrixTransformExample(PathKit) {

View File

@ -1,6 +1,6 @@
{
"name": "experimental-pathkit-asmjs",
"version": "0.2.0",
"version": "0.3.0",
"description": "A asm.js version of Skia's PathOps toolkit",
"main": "bin/pathkit.js",
"homepage": "https://github.com/google/skia/tree/master/experimental/pathkit",

View File

@ -37,6 +37,7 @@
<canvas class=big id=canvas8></canvas>
<canvas class=big id=canvas9></canvas>
<canvas class=big id=canvas10></canvas>
<canvas class=big id=canvas11></canvas>
<canvas class=big id=canvasTransform></canvas>
<h2> Supports fill-rules of nonzero and evenodd </h2>
@ -193,58 +194,74 @@
// no-op
(path) => path,
// dash
(path) => path.dash(10, 3, 0),
(path, counter) => path.dash(10, 3, counter/5),
// trim (takes optional 3rd param for returning the trimmed part
// or the complement)
(path) => path.trim(0.25, 0.8, false),
(path, counter) => path.trim((counter/100) % 1, 0.8, false),
// simplify
(path) => path.simplify(),
// stroke
(path) => path.stroke({
width: 15,
(path, counter) => path.stroke({
width: 10 * (Math.sin(counter/30) + 1),
join: PathKit.StrokeJoin.BEVEL,
cap: PathKit.StrokeCap.BUTT,
miter_limit: 1,
}),
// "offset effect", that is, making a border around the shape.
(path) => {
(path, counter) => {
let orig = path.copy();
path.stroke({
width: 10,
width: 10 + (counter / 4) % 50,
join: PathKit.StrokeJoin.ROUND,
cap: PathKit.StrokeCap.SQUARE,
})
.op(orig, PathKit.PathOp.DIFFERENCE);
orig.delete();
},
(path, counter) => {
let simplified = path.simplify().copy();
path.stroke({
width: 2 + (counter / 2) % 100,
join: PathKit.StrokeJoin.BEVEL,
cap: PathKit.StrokeCap.BUTT,
})
.op(simplified, PathKit.PathOp.REVERSE_DIFFERENCE);
simplified.delete();
}
];
let names = ["(plain)", "Dash", "Trim", "Simplify", "Stroke", "Offset"];
let names = ["(plain)", "Dash", "Trim", "Simplify", "Stroke", "Grow", "Shrink"];
for (let i = 0; i < effects.length; i++) {
let path = PathKit.NewPath();
drawStar(path);
let counter = 0;
function frame() {
counter++;
for (let i = 0; i < effects.length; i++) {
let path = PathKit.NewPath();
drawStar(path);
// The transforms apply directly to the path.
effects[i](path);
// The transforms apply directly to the path.
effects[i](path, counter);
let ctx = document.getElementById(`canvas${i+5}`).getContext('2d');
setCanvasSize(ctx, 300, 300);
ctx.strokeStyle = '#3c597a';
ctx.fillStyle = '#3c597a';
if (i === 4 || i === 5) {
ctx.fill(path.toPath2D(), path.getFillTypeString());
} else {
ctx.stroke(path.toPath2D());
let ctx = document.getElementById(`canvas${i+5}`).getContext('2d');
setCanvasSize(ctx, 300, 300);
ctx.strokeStyle = '#3c597a';
ctx.fillStyle = '#3c597a';
if (i >=4 ) {
ctx.fill(path.toPath2D(), path.getFillTypeString());
} else {
ctx.stroke(path.toPath2D());
}
ctx.font = '42px monospace';
let x = 150-ctx.measureText(names[i]).width/2;
ctx.strokeText(names[i], x, 290);
path.delete();
}
ctx.font = '42px monospace';
let x = 150-ctx.measureText(names[i]).width/2;
ctx.strokeText(names[i], x, 290);
path.delete();
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame(frame);
}
function MatrixTransformExample(PathKit) {

View File

@ -1,6 +1,6 @@
{
"name": "experimental-pathkit-wasm",
"version": "0.2.0",
"version": "0.3.0",
"description": "A WASM version of Skia's PathOps toolkit",
"main": "bin/pathkit.js",
"homepage": "https://github.com/google/skia/tree/master/experimental/pathkit",

View File

@ -134,7 +134,7 @@ SkPathOrNull EMSCRIPTEN_KEEPALIVE FromCmds(uintptr_t /* float* */ cptr, int numC
CHECK_NUM_ARGS(5);
x1 = cmds[i++], y1 = cmds[i++];
x2 = cmds[i++], y2 = cmds[i++];
x3 = cmds[i++]; // width
x3 = cmds[i++]; // weight
path.conicTo(x1, y1, x2, y2, x3);
break;
case CUBIC:
@ -249,6 +249,14 @@ bool EMSCRIPTEN_KEEPALIVE ApplyPathOp(SkPath& pathOne, const SkPath& pathTwo, Sk
return Op(pathOne, pathTwo, op, &pathOne);
}
SkPathOrNull EMSCRIPTEN_KEEPALIVE MakeFromOp(const SkPath& pathOne, const SkPath& pathTwo, SkPathOp op) {
SkPath out;
if (Op(pathOne, pathTwo, op, &out)) {
return emscripten::val(out);
}
return emscripten::val::null();
}
SkPathOrNull EMSCRIPTEN_KEEPALIVE ResolveBuilder(SkOpBuilder& builder) {
SkPath path;
if (builder.resolve(&path)) {
@ -481,9 +489,9 @@ EMSCRIPTEN_BINDINGS(skia) {
.function("_arcTo", &ApplyArcTo)
//"bezierCurveTo" alias handled in JS bindings
.function("_close", &ApplyClose)
//"closePath" alias handled in JS bindings
.function("_conicTo", &ApplyConicTo)
.function("_cubicTo", &ApplyCubicTo)
//"closePath" alias handled in JS bindings
.function("_ellipse", &ApplyEllipse)
.function("_lineTo", &ApplyLineTo)
@ -530,6 +538,7 @@ EMSCRIPTEN_BINDINGS(skia) {
.constructor<>()
.function("add", &SkOpBuilder::add)
.function("make", &ResolveBuilder)
.function("resolve", &ResolveBuilder);
// Without these function() bindings, the function would be exposed but oblivious to
@ -537,13 +546,14 @@ EMSCRIPTEN_BINDINGS(skia) {
// Import
function("FromSVGString", &FromSVGString);
function("FromCmds", &FromCmds);
function("NewPath", &NewPath);
function("NewPath", &CopyPath);
// FromCmds is defined in helper.js to make use of TypedArrays transparent.
function("_FromCmds", &FromCmds);
// Path2D is opaque, so we can't read in from it.
// PathOps
function("ApplyPathOp", &ApplyPathOp);
function("MakeFromOp", &MakeFromOp);
enum_<SkPathOp>("PathOp")
.value("DIFFERENCE", SkPathOp::kDifference_SkPathOp)
@ -574,7 +584,7 @@ EMSCRIPTEN_BINDINGS(skia) {
.field("fRight", &SkRect::fRight)
.field("fBottom", &SkRect::fBottom);
function("MakeLTRBRect", &SkRect::MakeLTRB);
function("LTRBRect", &SkRect::MakeLTRB);
// Stroke
enum_<SkPaint::Join>("StrokeJoin")

View File

@ -112,13 +112,13 @@ describe('PathKit\'s Path Behavior', function() {
LoadPathKit.then(() => {
// Based on test_bounds_crbug_513799
let path = PathKit.NewPath();
expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(0, 0, 0, 0));
expect(path.getBounds()).toEqual(PathKit.LTRBRect(0, 0, 0, 0));
path.moveTo(-5, -8);
expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, -5, -8));
expect(path.getBounds()).toEqual(PathKit.LTRBRect(-5, -8, -5, -8));
path.rect(1, 2, 2, 2);
expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, 3, 4));
expect(path.getBounds()).toEqual(PathKit.LTRBRect(-5, -8, 3, 4));
path.moveTo(1, 2);
expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(-5, -8, 3, 4));
expect(path.getBounds()).toEqual(PathKit.LTRBRect(-5, -8, 3, 4));
path.delete();
done();
});
@ -130,9 +130,9 @@ describe('PathKit\'s Path Behavior', function() {
let path = PathKit.NewPath();
path.moveTo(1, 1);
path.quadraticCurveTo(4, 3, 2, 2);
expect(path.getBounds()).toEqual(PathKit.MakeLTRBRect(1, 1, 4, 3));
expect(path.getBounds()).toEqual(PathKit.LTRBRect(1, 1, 4, 3));
ExpectRectsToBeEqual(path.computeTightBounds(),
PathKit.MakeLTRBRect(1, 1,
PathKit.LTRBRect(1, 1,
bits2float("0x40333334"), // 2.8
bits2float("0x40155556"))); // 2.3333333
path.delete();

View File

@ -117,11 +117,6 @@ describe('PathKit\'s PathOps Behavior', function() {
return e;
}
function fromCmds(cmds) {
let [ptr, len] = PathKit.loadCmdsTypedArray(cmds);
return PathKit.FromCmds(ptr, len);
}
it('combines two paths with .op() and matches what we see from C++', function(done) {
LoadPathKit.then(() => {
// Test JSON created with:
@ -135,11 +130,11 @@ describe('PathKit\'s PathOps Behavior', function() {
for (testName of testNames) {
let test = json[testName];
let path1 = fromCmds(test.p1);
let path1 = PathKit.FromCmds(test.p1);
expect(path1).not.toBeNull(`path1 error when loading cmds '${test.p1}'`);
path1.setFillType(getFillType(test.fillType1));
let path2 = fromCmds(test.p2);
let path2 = PathKit.FromCmds(test.p2);
expect(path2).not.toBeNull(`path2 error when loading cmds '${test.p2}'`);
path2.setFillType(getFillType(test.fillType2));
@ -149,7 +144,7 @@ describe('PathKit\'s PathOps Behavior', function() {
expect(combined).toBeNull(`Test ${testName} should have not created output, but did`);
} else {
expect(combined).not.toBeNull();
let expected = fromCmds(test.out);
let expected = PathKit.FromCmds(test.out);
// Do a tolerant match.
let diff = diffPaths(expected, combined);
if (test.expectMatch === 'yes'){
@ -192,7 +187,7 @@ describe('PathKit\'s PathOps Behavior', function() {
for (testName of testNames) {
let test = json[testName];
let path = fromCmds(test.path);
let path = PathKit.FromCmds(test.path);
expect(path).not.toBeNull(`path1 error when loading cmds '${test.path}'`);
path.setFillType(getFillType(test.fillType));
@ -202,7 +197,7 @@ describe('PathKit\'s PathOps Behavior', function() {
expect(simplified).toBeNull(`Test ${testName} should have not created output, but did`);
} else {
expect(simplified).not.toBeNull();
let expected = fromCmds(test.out);
let expected = PathKit.FromCmds(test.out);
// Do a tolerant match.
let diff = diffPaths(expected, simplified);
if (test.expectMatch === 'yes'){

View File

@ -45,8 +45,7 @@ describe('PathKit\'s SVG Behavior', function() {
[PathKit.LINE_VERB, 5, 295],
[PathKit.LINE_VERB, 205, 5],
[PathKit.CLOSE_VERB]];
let [ptr, len] = PathKit.loadCmdsTypedArray(cmds);
let path = PathKit.FromCmds(ptr, len);
let path = PathKit.FromCmds(cmds);
let svgStr = path.toSVGString();
// We output it in terse form, which is different than Wikipedia's version
@ -64,8 +63,7 @@ describe('PathKit\'s SVG Behavior', function() {
[PathKit.LINE_VERB, 5, 295],
[PathKit.LINE_VERB, "0x15e80300", "0x400004dc"], // 9.37088e-26f, 2.0003f
[PathKit.CLOSE_VERB]];
let [ptr, len] = PathKit.loadCmdsTypedArray(cmds);
let path = PathKit.FromCmds(ptr, len);
let path = PathKit.FromCmds(cmds);
let svgStr = path.toSVGString();
expect(svgStr).toEqual('M9.37088e-26 2.0003L795 5L595 295L5 295L9.37088e-26 2.0003Z');

View File

@ -7,23 +7,774 @@ available to JS clients (e.g. Web Browsers) using WebAssembly and asm.js.
Download the library
--------------------
See [the npm page](https://www.npmjs.com/package/experimental-pathkit-wasm) for details on downloading
and getting started.
See the the npm page for either the [WebAssembly](https://www.npmjs.com/package/experimental-pathkit-wasm) version
or the [asm.js](https://www.npmjs.com/package/experimental-pathkit-asmjs) version
for details on downloading and getting started.
WebAssembly has faster load times and better overall performance but is
currently supported by only Chrome and Firefox (with a flag).
The asm.js version should run anywhere JavaScript does.
Features
--------
PathKit is still under rapid development, so the exact API is still changing.
PathKit is still under rapid development, so the exact API is subject to change.
The primary features are:
- API compatibility (e.g. drop-in replacement) with [Path2D](https://developer.mozilla.org/en-US/docs/Web/API/Path2D)
- Can output to SVG / Canvas / Path2D
- Exposes a variety of path effects: <img width=800 src="./PathKit_effects.png"/>
- Exposes a variety of path effects:
<style>
canvas.patheffect {
border: 1px dashed #AAA;
width: 200px;
height: 200px;
}
</style>
<div id=effects>
<canvas class=patheffect id=canvas1 title="Plain: A drawn star with overlapping solid lines"></canvas>
<canvas class=patheffect id=canvas2 title="Dash: A drawn star with overlapping dashed lines"></canvas>
<canvas class=patheffect id=canvas3 title="Trim: A portion of a drawn star with overlapping solid lines"></canvas>
<canvas class=patheffect id=canvas4 title="Simplify: A drawn star with non-overlapping solid lines."></canvas>
<canvas class=patheffect id=canvas5 title="Stroke: A drawn star with non-overlapping solid lines stroked at various thicknesses and with square edges"></canvas>
<canvas class=patheffect id=canvas6 title="Grow: A drawn star's expanding outline"></canvas>
<canvas class=patheffect id=canvas7 title="Shrink: A solid drawn star shrunk down"></canvas>
<canvas class=patheffect id=canvasTransform title="Transform: A drawn star moved and rotated by an Affine Matrix"></canvas>
</div>
<script src="https://unpkg.com/experimental-pathkit-asmjs@0.3.0/bin/pathkit.js"></script>
<script>
try {
PathKitInit({
locateFile: (file) => 'https://unpkg.com/experimental-pathkit-asmjs@0.3.0/bin/'+file,
}).then((PathKit) => {
// Code goes here using PathKit
PathEffectsExample(PathKit);
MatrixTransformExample(PathKit);
});
}
catch(error) {
console.warn(error, 'falling back to image');
docment.getElementById('effects').innerHTML = '<img width=800 src="./PathKit_effects.png"/>'
}
function setCanvasSize(ctx, width, height) {
ctx.canvas.width = width;
ctx.canvas.height = height;
}
function drawStar(path) {
let R = 115.2, C = 128.0;
path.moveTo(C + R + 22, C);
for (let i = 1; i < 8; i++) {
let a = 2.6927937 * i;
path.lineTo(C + R * Math.cos(a) + 22, C + R * Math.sin(a));
}
path.closePath();
return path;
}
function PathEffectsExample(PathKit) {
let effects = [
// no-op
(path) => path,
// dash
(path, counter) => path.dash(10, 3, counter/5),
// trim (takes optional 3rd param for returning the trimmed part
// or the complement)
(path, counter) => path.trim((counter/100) % 1, 0.8, false),
// simplify
(path) => path.simplify(),
// stroke
(path, counter) => path.stroke({
width: 10 * (Math.sin(counter/30) + 1),
join: PathKit.StrokeJoin.BEVEL,
cap: PathKit.StrokeCap.BUTT,
miter_limit: 1,
}),
// "offset effect", that is, making a border around the shape.
(path, counter) => {
let orig = path.copy();
path.stroke({
width: 10 + (counter / 4) % 50,
join: PathKit.StrokeJoin.ROUND,
cap: PathKit.StrokeCap.SQUARE,
})
.op(orig, PathKit.PathOp.DIFFERENCE);
orig.delete();
},
(path, counter) => {
let simplified = path.simplify().copy();
path.stroke({
width: 2 + (counter / 2) % 100,
join: PathKit.StrokeJoin.BEVEL,
cap: PathKit.StrokeCap.BUTT,
})
.op(simplified, PathKit.PathOp.REVERSE_DIFFERENCE);
simplified.delete();
}
];
let names = ["(plain)", "Dash", "Trim", "Simplify", "Stroke", "Grow", "Shrink"];
let counter = 0;
function frame() {
counter++;
for (let i = 0; i < effects.length; i++) {
let path = PathKit.NewPath();
drawStar(path);
// The transforms apply directly to the path.
effects[i](path, counter);
let ctx = document.getElementById(`canvas${i+1}`).getContext('2d');
setCanvasSize(ctx, 300, 300);
ctx.strokeStyle = '#3c597a';
ctx.fillStyle = '#3c597a';
if (i >=4 ) {
ctx.fill(path.toPath2D(), path.getFillTypeString());
} else {
ctx.stroke(path.toPath2D());
}
ctx.font = '42px monospace';
let x = 150-ctx.measureText(names[i]).width/2;
ctx.strokeText(names[i], x, 290);
path.delete();
}
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame(frame);
}
function MatrixTransformExample(PathKit) {
// Creates an animated star that twists and moves.
let ctx = document.getElementById('canvasTransform').getContext('2d');
setCanvasSize(ctx, 300, 300);
ctx.strokeStyle = '#3c597a';
let path = drawStar(PathKit.NewPath());
// TODO(kjlubick): Perhaps expose some matrix helper functions to allow
// clients to build their own matrices like this?
// These matrices represent a 2 degree rotation and a 1% scale factor.
let scaleUp = [1.0094, -0.0352, 3.1041,
0.0352, 1.0094, -6.4885,
0 , 0 , 1];
let scaleDown = [ 0.9895, 0.0346, -2.8473,
-0.0346, 0.9895, 6.5276,
0 , 0 , 1];
let i = 0;
function frame(){
i++;
if (Math.round(i/100) % 2) {
path.transform(scaleDown);
} else {
path.transform(scaleUp);
}
ctx.clearRect(0, 0, 300, 300);
ctx.stroke(path.toPath2D());
ctx.font = '42px monospace';
let x = 150-ctx.measureText('Transform').width/2;
ctx.strokeText('Transform', x, 290);
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame(frame);
}
</script>
Example code
Example Code
------------
The best place to look for examples on how to use PathKit would be in the
[example.html](https://github.com/google/skia/blob/master/experimental/pathkit/npm-wasm/example.html#L45)
which comes in the npm package.
[example.html](https://github.com/google/skia/blob/master/experimental/pathkit/npm-wasm/example.html#L45),
which comes in the npm package.
API
----
The primary feature of the library is the `SkPath` object. It can be created:
- From the SVG string of a path `PathKit.FromSVGString(str)`
- From a 2D array of verbs and arguments `PathKit.FromCmds(cmds)`
- From `PathKit.NewPath()` (It will be blank)
- As a copy of an existing `SkPath` with `path.copy()` or `PathKit.NewPath(path)`
It can be exported as:
- An SVG string `path.toSVGString()`
- A [Path2D](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) object `path.toPath2D()`
- Directly to a canvas 2D context `path.toCanvas(ctx)`
- A 2D array of verbs and arguments `path.toCmds()`
Once an SkPath object has been made, it can be interacted with in the following ways:
- expanded by any of the Path2D operations (`moveTo`, `lineTo`, `rect`, `arc`, etc)
- combined with other paths using `op` or `PathKit.MakeFromOp(p1, p2, op)`. For example, `path1.op(path2, PathKit.PathOp.INTERSECT)` will set path1 to be the area represented by where path1 and path2 overlap (intersect). `PathKit.MakeFromOp(path1, path2, PathKit.PathOp.INTERSECT)` will do the same but returned as a new `SkPath` object.
- adjusted with some of the effects (`trim`, `dash`, `stroke`, etc)
**Important**: Any objects (`SkPath`, `SkOpBuilder`, etc) that are created must be cleaned up with `path.delete()` when they
leave the scope to avoid leaking the memory in the WASM heap. This includes any of the constructors, `copy()`,
or any function prefixed with "make".
### PathKit ###
#### `FromSVGString(str)` ####
**str** - `String` representing an [SVGPath](https://www.w3schools.com/graphics/svg_path.asp)
Returns an `SkPath` with the same verbs and arguments as the SVG string, or `null` on a failure.
Example:
let path = PathKit.FromSVGString('M150 0 L75 200 L225 200 Z');
// path represents a triangle
// don't forget to do path.delete() when it goes out of scope.
#### `FromCmds(cmds)` ####
**cmds** - `Array<Array<Number>>`, a 2D array of commands, where a command is a verb
followed by its arguments.
Returns an `SkPath` with the verbs and arguments from the list or `null` on a failure.
This can be faster than calling `.moveTo()`, `.lineTo()`, etc many times.
Example:
let cmds = [
[PathKit.MOVE_VERB, 0, 10],
[PathKit.LINE_VERB, 30, 40],
[PathKit.QUAD_VERB, 20, 50, 45, 60],
];
let path = PathKit.FromCmds(cmds);
// path is the same as if a user had done
// let path = PathKit.NewPath().moveTo(0, 10).lineTo(30, 40).quadTo(20, 50, 45, 60);
// don't forget to do path.delete() when it goes out of scope.
#### `NewPath()` ####
Returns an empty `SkPath` object.
Example:
let path = PathKit.NewPath();
path.moveTo(0, 10)
.lineTo(30, 40)
.quadTo(20, 50, 45, 60);
// don't forget to do path.delete() when it goes out of scope.
// Users can also do let path = new PathKit.SkPath();
#### `NewPath(pathToCopy)` ####
**pathToCopy** - SkPath, a path to make a copy of.
Returns a `SkPath` that is a copy of the passed in `SkPath`.
Example:
let otherPath = ...;
let clone = PathKit.NewPath(otherPath);
clone.simplify();
// don't forget to do clone.delete() when it goes out of scope.
// Users can also do let clone = new PathKit.SkPath(otherPath);
// or let clone = otherPath.copy();
#### `MakeFromOp(pathOne, pathTwo, op)` ####
**pathOne** - `SkPath`, a path. <br>
**pathTwo** - `SkPath`, a path. <br>
**op** - `PathOp`, an op to apply
Returns a new `SkPath` that is the result of applying the given PathOp to the first and second
path (order matters).
Example:
let pathOne = PathKit.NewPath().moveTo(0, 20).lineTo(10, 10).lineTo(20, 20).close();
let pathTwo = PathKit.NewPath().moveTo(10, 20).lineTo(20, 10).lineTo(30, 20).close();
let mountains = PathKit.MakeFromOp(pathOne, pathTwo, PathKit.PathOp.UNION);
// don't forget to do mountains.delete() when it goes out of scope.
// Users can also do pathOne.op(pathTwo, PathKit.PathOp.UNION);
// to have the resulting path be stored to pathOne and avoid allocating another object.
### SkPath (object) ###
#### `addPath(otherPath)` ####
**otherPath** - `SkPath`, a path to append to this path
Adds the given path to `this` and then returns `this` for chaining purposes.
#### `addPath(otherPath, transform)` ####
**otherPath** - `SkPath`, a path to append to this path. <br>
**transform** - [SVGMatrix](https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix),
a transform to apply to otherPath before appending it.
Adds the given path to `this` after applying the transform and then returns `this` for
chaining purposes. See [Path2D.addPath()](https://developer.mozilla.org/en-US/docs/Web/API/Path2D/addPath)
for more details.
#### `addPath(otherPath, a, b, c, d, e, f)` ####
**otherPath** - `SkPath`, a path to append to this path. <br>
**a, b, c, d, e, f** - `Number`, the six components of an
[SVGMatrix](https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix),
which define the transform to apply to otherPath before appending it.
Adds the given path to `this` after applying the transform and then returns `this` for
chaining purposes. See [Path2D.addPath()](https://developer.mozilla.org/en-US/docs/Web/API/Path2D/addPath)
for more details.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
let moreBoxes = PathKit.NewPath();
// add box un-transformed (i.e. at 0, 0)
moreBoxes.addPath(box)
// the params fill out a 2d matrix like:
// a c e
// b d f
// 0 0 1
// add box 300 points to the right
.addPath(box, 1, 0, 0, 1, 300, 0)
// add a box shrunk by 50% in both directions
.addPath(box, 0.5, 0, 0, 0.5, 0, 0);
// moreBoxes now has 3 paths appended to it
#### `addPath(otherPath, scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2)` ####
**otherPath** - `SkPath`, a path to append to this path. <br>
**scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2** -
`Number`, the nine components of an
[Affine Matrix](https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations),
which define the transform to apply to otherPath before appending it.
Adds the given path to `this` after applying the transform and then returns `this` for
chaining purposes.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
let moreBoxes = PathKit.NewPath();
// add box un-transformed (i.e. at 0, 0)
moreBoxes.addPath(box)
// add box 300 points to the right
.addPath(box, 1, 0, 0,
0, 1, 300,
0, 0 ,1)
// add a box shrunk by 50% in both directions
.addPath(box, 0.5, 0, 0,
0, 0.5, 0,
0, 0, 1)
// moreBoxes now has 3 paths appended to it
#### `arc(x, y, radius, startAngle, endAngle, ccw=false)` ####
**x, y** - `Number`, The coordinates of the arc's center. <br>
**radius** - `Number`, The radius of the arc. <br>
**startAngle, endAngle** - `Number`, the start and end of the angle, measured
clockwise from the positive x axis and in radians. <br>
**ccw** - `Boolean`, optional argument specifying if the arc should be drawn
counter-clockwise between **startAngle** and **endAngle** instead of
clockwise, the default.
Adds the described arc to `this` then returns `this` for
chaining purposes. See [Path2D.arc()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc)
for more details.
Example:
let path = PathKit.NewPath();
path.moveTo(20, 120);
.arc(20, 120, 18, 0, 1.75 * Math.PI);
.lineTo(20, 120);
// path looks like a pie with a 1/8th slice removed.
#### `arcTo(x1, y1, x2, y2, radius)` ####
**x1, y1, x2, y2** - `Number`, The coordinates defining the control points. <br>
**radius** - `Number`, The radius of the arc.
Adds the described arc to `this` (appending a line, if needed) then returns `this` for
chaining purposes. See [Path2D.arcTo()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arcTo)
for more details.
#### `close()` or `closePath()` ####
Returns the pen to the start of the current sub-path, then returns `this` for
chaining purposes. See [Path2D.closePath()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/closePath)
for more details.
#### `computeTightBounds()` ####
Returns an `SkRect` that represents the minimum and maximum area of
`this` path. See [SkPath reference](https://skia.org/user/api/SkPath_Reference#SkPath_computeTightBounds)
for more details.
#### `conicTo(x1, y1, x2, y2, w)` ####
**x1, y1, x2, y2** - `Number`, The coordinates defining the control point and the end point. <br>
**w** - `Number`, The weight of the conic.
Adds the described conic line to `this` (appending a line, if needed) then returns `this` for
chaining purposes. See [SkPath reference](https://skia.org/user/api/SkPath_Reference#SkPath_conicTo)
for more details.
#### `copy()` ####
Return a copy of `this` path.
#### `cubicTo(cp1x, cp1y, cp2x, cp2y, x, y)` or `bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)` ####
**cp1x, cp1y, cp2x, cp2y** - `Number`, The coordinates defining the control points. <br>
**x,y** - `Number`, The coordinates defining the end point
Adds the described cubic line to `this` (appending a line, if needed) then returns `this` for
chaining purposes. See [Path2D.bezierCurveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/bezierCurveTo)
for more details.
#### `dash(on, off, phase)` ####
**on, off** - `Number`, The number of pixels the dash should be on (drawn) and off (blank). <br>
**phase** - `Number`, The number of pixels the on/off should be offset (mod **on** + **off**)
Applies a dashed path effect to `this` then returns `this` for chaining purposes.
See the "Dash" effect above for a visual example.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
box.dash(20, 10, 3);
// box is now a dashed rectangle that will draw for 20 pixels, then
// stop for 10 pixels. Since phase is 3, the first line won't start
// at (0, 0), but 3 pixels around the path (3, 0)
#### `ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, ccw=false)` ####
**x, y** - `Number`, The coordinates of the center of the ellipse. <br>
**radiusX, radiusY** - `Number`, The radii in the X and Y directions. <br>
**rotation** - `Number`, The rotation in radians of this ellipse. <br>
**startAngle, endAngle** - `Number`, the starting and ending angles of which to draw,
measured in radians from the positive x axis. <br>
**ccw** - `Boolean`, optional argument specifying if the ellipse should be drawn
counter-clockwise between **startAngle** and **endAngle** instead of
clockwise, the default.
Adds the described ellipse to `this` then returns `this` for chaining purposes.
See [Path2D.ellipse](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/ellipse)
for more details.
#### `equals(otherPath)` ####
**otherPath** - `SkPath`, the path to compare to.
Returns a `Boolean` value based on if `this` path is equal
to **otherPath**.
#### `getBounds()` ####
Returns an `SkRect` that represents the minimum and maximum area of
`this` path. See [SkPath reference](https://skia.org/user/api/SkPath_Reference#SkPath_getBounds)
for more details.
#### `getFillType()` ####
Returns a `FillType` based on what this path is. This defaults to
`PathKit.FillType.WINDING`, but may change with `op()` or `simplify()`.
Clients will typically want `getFillTypeString()` because that value
can be passed directly to an SVG or Canvas.
#### `getFillTypeString()` ####
Returns a `String` representing the fillType of `this` path.
The values are either "nonzero" or "evenodd".
Example:
let path = ...;
let ctx = document.getElementById('canvas1').getContext('2d');
ctx.strokeStyle = 'green';
ctx.fill(path.toPath2D(), path.getFillTypeString());
#### `moveTo(x, y)` ####
**x, y** - `Number`, The coordinates of where the pen should be moved to.
Moves the pen (without drawing) to the given coordinates then returns `this` for chaining purposes.
See [Path2D.moveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/moveTo)
for more details.
#### `lineTo(x, y)` ####
**x, y** - `Number`, The coordinates of where the pen should be moved to.
Draws a straight line to the given coordinates then returns `this` for chaining purposes.
See [Path2D.lineTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineTo)
for more details.
#### `op(otherPath, operation)` ####
**otherPath** - `SkPath`, The other path to be combined with `this`. <br>
**operation** - `PathOp`, The operation to apply to the two paths.
Combines otherPath into `this` path with the given operation and returns `this`
for chaining purposes.
Example:
let pathOne = PathKit.NewPath().moveTo(0, 20).lineTo(10, 10).lineTo(20, 20).close();
let pathTwo = PathKit.NewPath().moveTo(10, 20).lineTo(20, 10).lineTo(30, 20).close();
// Combine the two triangles to look like two mountains
let mountains = pathOne.copy().op(pathOne, pathTwo, PathKit.PathOp.UNION);
// set pathOne to be the small triangle where pathOne and pathTwo overlap
pathOne.op(pathOne, pathTwo, PathKit.PathOp.INTERSECT);
// since copy() was called, don't forget to call delete() on mountains.
#### `quadTo(cpx, cpy, x, y)` or `quadraticCurveTo(cpx, cpy, x, y)` ####
**cpx, cpy** - `Number`, The coordinates for the control point. <br>
**x, y** - `Number`, The coordinates for the end point.
Draws a quadratic Bézier curve with the given coordinates then returns `this` for chaining purposes.
See [Path2D.quadraticCurveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/quadraticCurveTo)
for more details.
#### `rect(x, y, w, h)` ####
**x, y** - `Number`, The coordinates of the upper-left corner of the rectangle. <br>
**w, h** - `Number`, The width and height of the rectangle
Draws a rectangle on `this`, then returns `this` for chaining purposes.
See [Path2D.rect](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rect)
for more details.
#### `setFillType(fillType)` ####
**fillType** - `FillType`, the new fillType.
Set the fillType of the path. See [SkPath reference](https://skia.org/user/api/SkPath_Reference#SkPath_FillType)
for more details.
#### `simplify()` ####
Set `this` path to a set of *non-overlapping* contours that describe the same area
as the original path. See the "Simplify" effect above for a visual example.
#### `stroke(opts)` ####
**opts** - `StrokeOpts`, contains the options for stroking.
Strokes `this` path out with the given options. This can be used for a variety of
effects. See the "Stroke", "Grow", and "Shrink" effects above for visual examples.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
// Stroke the path with width 10 and rounded corners
let rounded = box.copy().stroke({width: 10, join: PathKit.StrokeJoin.ROUND});
// Grow effect, that is, a 20 pixel expansion around the box.
let grow = box.copy().stroke({width: 20}).op(box, PathKit.PathOp.DIFFERENCE);
// Shrink effect, in which we subtract away from the original
let simplified = box.copy().simplify(); // sometimes required for complicated paths
let shrink = box.copy().stroke({width: 15, cap: PathKit.StrokeCap.BUTT})
.op(simplified, PathKit.PathOp.REVERSE_DIFFERENCE);
// Don't forget to call delete() on each of the copies!
#### `toCanvas(ctx)` ####
**ctx** - `Canvas2DContext`, Canvas on which to draw the path.
Draws `this` path on the passed in
[Canvas Context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D).
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
let ctx = document.getElementById('canvas1').getContext('2d');
ctx.strokeStyle = 'green';
ctx.beginPath();
box.toCanvas(ctx);
ctx.stroke(); // could also ctx.fill()
#### `toCmds()` ####
Returns a 2D Array of verbs and args. See `PathKit.FromCmds()` for
more details.
#### `toPath2D()` ####
Returns a [Path2D](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) object
that has the same operations as `this` path.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
let ctx = document.getElementById('canvas1').getContext('2d');
ctx.strokeStyle = 'green';
ctx.stroke(box.toPath2D());
#### `toSVGString()` ####
Returns a `String` representing an [SVGPath](https://www.w3schools.com/graphics/svg_path.asp) based on `this` path.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
let svg = document.getElementById('svg1');
let newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
newPath.setAttribute('stroke', 'green');
newPath.setAttribute('fill', 'white');
newPath.setAttribute('d', box.toSVGString());
svg.appendChild(newPath);
#### `transform(matr)` ####
**matr** - `SkMatrix`, i.e. an `Array<Number>` of the nine numbers of an Affine Transform Matrix.
Applies the specified [transform](https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations)
to `this` and then returns `this` for chaining purposes.
#### `transform(scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2)` ####
**scaleX, skewX, transX, skewY, scaleY, transY, pers0, pers1, pers2** -
`Number`, the nine numbers of an Affine Transform Matrix.
Applies the specified [transform](https://en.wikipedia.org/wiki/Transformation_matrix#Affine_transformations)
to `this` and then returns `this` for chaining purposes.
Example:
let path = PathKit.NewPath().rect(0, 0, 100, 100);
// scale up the path by 5x
path.transform([5, 0, 0,
0, 5, 0,
0, 0, 1]);
// move the path 75 px to the right.
path.transform(1, 0, 75,
0, 1, 0,
0, 0, 1);
#### `trim(startT, stopT, isComplement=false)` ####
**startT, stopT** - `Number`, values in [0, 1] that indicate the start and stop
"percentages" of the path to draw <br>
**isComplement** - `Boolean`, If the complement of the trimmed section should
be drawn instead of the areas between **startT** and **stopT**.
Sets `this` path to be a subset of the original path, then returns `this` for chaining purposes.
See the "Trim" effect above for a visual example.
Example:
let box = PathKit.NewPath().rect(0, 0, 100, 100);
box.trim(0.25, 1.0);
// box is now the 3 segments that look like a U
// (the top segment has been removed).
### SkOpBuilder (object) ###
This object enables chaining multiple PathOps together.
Create one with `let builder = new PathKit.SkOpBuilder();`
When created, the internal state is "empty path".
Don't forget to call `delete()` on both the builder and the result
of `resolve()`
#### `add(path, operation)` ####
**path** - `SkPath`, The path to be combined with the given rule. <br>
**operation** - `PathOp`, The operation to apply to the two paths.
Adds a path and the operand to the builder.
#### `make()` or `resolve()` ####
Creates and returns a new `SkPath` based on all the given paths
and operands.
Don't forget to call `.delete()` on the returned path when it goes out of scope.
### SkMatrix (struct) ###
`SkMatrix` translates between a C++ struct and a JS Array.
It basically takes a nine element 1D Array and turns it into a
3x3 2D Affine Matrix.
### SkRect (struct) ###
`SkRect` translates between a C++ struct and a JS Object with
the following keys (all values are `Number`:
- **fLeft**: x coordinate of top-left corner
- **fTop**: y coordinate of top-left corner
- **fRight**: x coordinate of bottom-right corner
- **fBottom**: y coordinate of bottom-rightcorner
### StrokeOpts (struct) ###
`StrokeOpts` translates between a C++ struct and a JS Object with
the following keys:
- **width**, `Number` the width of the lines of the path. Default 1.
- **miter_limit**, `Number`, the miter limit. Defautl 4. See [SkPaint reference](https://skia.org/user/api/SkPaint_Reference#Miter_Limit) for more details.
- **join**, `StrokeJoin`, the join to use. Default `PathKit.StrokeJoin.MITER`.
See [SkPaint reference](https://skia.org/user/api/SkPaint_Reference#SkPaint_Join) for more details.
- **cap**, `StrokeCap`, the cap to use. Default `PathKit.StrokeCap.BUTT`.
See [SkPaint reference](https://skia.org/user/api/SkPaint_Reference#Stroke_Cap) for more details.
### PathOp (enum) ###
The following enum values are exposed. They are essentially constant
objects, differentiated by thier `.value` property.
- `PathKit.PathOp.DIFFERENCE`
- `PathKit.PathOp.INTERSECT`
- `PathKit.PathOp.REVERSE_DIFFERENCE`
- `PathKit.PathOp.UNION`
- `PathKit.PathOp.XOR`
These are used in `PathKit.MakeFromOp()` and `SkPath.op()`.
### FillType (enum) ###
The following enum values are exposed. They are essentially constant
objects, differentiated by thier `.value` property.
- `PathKit.FillType.WINDING` (also known as nonzero)
- `PathKit.FillType.EVENODD`
- `PathKit.FillType.INVERSE_WINDING`
- `PathKit.FillType.INVERSE_EVENODD`
These are used by `SkPath.getFillType()` and `SkPath.setFillType()`, but
generally clients will want `SkPath.getFillTypeString()`.
### StrokeJoin (enum) ###
The following enum values are exposed. They are essentially constant
objects, differentiated by thier `.value` property.
- `PathKit.StrokeJoin.MITER`
- `PathKit.StrokeJoin.ROUND`
- `PathKit.StrokeJoin.BEVEL`
See [SkPaint reference](https://skia.org/user/api/SkPaint_Reference#SkPaint_Join) for more details.
### StrokeCap (enum) ###
The following enum values are exposed. They are essentially constant
objects, differentiated by thier `.value` property.
- `PathKit.StrokeCap.BUTT`
- `PathKit.StrokeCap.ROUND`
- `PathKit.StrokeCap.SQUARE`
See [SkPaint reference](https://skia.org/user/api/SkPaint_Reference#Stroke_Cap) for more details.
### Constants ###
The following constants are exposed:
- `PathKit.MOVE_VERB` = 0
- `PathKit.LINE_VERB` = 1
- `PathKit.QUAD_VERB` = 2
- `PathKit.CONIC_VERB` = 3
- `PathKit.CUBIC_VERB` = 4
- `PathKit.CLOSE_VERB` = 5
These are only needed for `PathKit.FromCmds()`.
### Functions for testing only ###
#### `PathKit.LTRBRect(left, top, right, bottom)` ####
**left** - `Number`, x coordinate of top-left corner of the `SkRect`. <br>
**top** - `Number`, y coordinate of top-left corner of the `SkRect`. <br>
**right** - `Number`, x coordinate of bottom-right corner of the `SkRect`. <br>
**bottom** - `Number`, y coordinate of bottom-right corner of the `SkRect`.
Returns an `SkRect` object with the given params.
#### `SkPath.dump()` ####
Prints all the verbs and arguments to the console.
Only available on Debug and Test builds.