Skip to content

Commit

Permalink
feat(path2d): implement pathkit functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed Apr 16, 2021
1 parent 67f4443 commit eea95bf
Show file tree
Hide file tree
Showing 22 changed files with 775 additions and 121 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ jobs:
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: failure-images-x86_64-unknown-linux-gnu-${{ matrix.node }}
name: failure-images-x86_64-unknown-linux-musl-${{ matrix.node }}
path: __test__/failure/**

test-linux-aarch64-gnu-binding:
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ export class Path2D {
moveTo(x: number, y: number): void
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
rect(x: number, y: number, w: number, h: number): void

// PathKit methods
op(path: Path2D, operation: PathOp): Path2D
toSVGString(): string
getFillType(): FillType
setFillType(type: FillType): void
simplify(): Path2D
asWinding(): Path2D
stroke(stroke?: StrokeOptions): Path2D
transform(transform: DOMMatrix2DInit): Path2D
getBounds(): [left: number, top: number, right: number, bottom: number]
computeTightBounds(): [left: number, top: number, right: number, bottom: number]
trim(start: number, end: number, isComplement?: boolean): Path2D
equals(path: Path2D): boolean
}
```

Expand Down
6 changes: 3 additions & 3 deletions __test__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ava, { TestInterface } from 'ava'

import { createCanvas, Path2D } from '../index'
import { createCanvas, Path2D, Canvas, SKRSContext2D } from '../index'

const test = ava as TestInterface<{
canvas: HTMLCanvasElement
ctx: CanvasRenderingContext2D
canvas: Canvas
ctx: SKRSContext2D
}>

test.beforeEach((t) => {
Expand Down
3 changes: 3 additions & 0 deletions __test__/mountain.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 137 additions & 0 deletions __test__/pathkit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import test from 'ava'

import { FillType, Path2D, PathOp, StrokeCap, StrokeJoin } from '../index'

test('should be able to call toSVGString', (t) => {
const path = new Path2D()
path.rect(0, 0, 100, 100)
t.is(path.toSVGString(), 'M0 0L100 0L100 100L0 100L0 0Z')
})

test('should be able to create mountain via op', (t) => {
const pathOne = new Path2D()
const pathTwo = new Path2D()
pathOne.moveTo(0, 20)
pathOne.lineTo(10, 10)
pathOne.lineTo(20, 20)
pathOne.closePath()
pathTwo.moveTo(10, 20)
pathTwo.lineTo(20, 10)
pathTwo.lineTo(30, 20)
pathTwo.closePath()
t.is(pathOne.op(pathTwo, PathOp.Union).toSVGString(), 'M10 10L0 20L30 20L20 10L15 15L10 10Z')
})

test('FillType must be Winding after conversion by AsWinding()', (t) => {
const path = new Path2D()
path.rect(1, 2, 3, 4)
path.setFillType(FillType.EvenOdd)
t.is(path.asWinding().getFillType(), FillType.Winding)
})

test('Path FillType must be converted from nonzero to evenodd', (t) => {
const pathCircle = new Path2D(
'M50 87.5776C70.7536 87.5776 87.5776 70.7536 87.5776 50C87.5776 29.2464 70.7536 12.4224 50 12.4224C29.2464 12.4224 12.4224 29.2464 12.4224 50C12.4224 70.7536 29.2464 87.5776 50 87.5776ZM50 100C77.6142 100 100 77.6142 100 50C100 22.3858 77.6142 0 50 0C22.3858 0 0 22.3858 0 50C0 77.6142 22.3858 100 50 100Z',
)
const nonzeroPathCircle =
'M50 87.5776C29.2464 87.5776 12.4224 70.7536 12.4224 50C12.4224 29.2464 29.2464 12.4224 50 12.4224C70.7536 12.4224 87.5776 29.2464 87.5776 50C87.5776 70.7536 70.7536 87.5776 50 87.5776ZM50 100C77.6142 100 100 77.6142 100 50C100 22.3858 77.6142 0 50 0C22.3858 0 0 22.3858 0 50C0 77.6142 22.3858 100 50 100Z'

pathCircle.setFillType(FillType.EvenOdd) // The FillType of the original path is evenodd

t.is(pathCircle.asWinding().toSVGString(), nonzeroPathCircle)
})

test('Use .simplify() to convert cubic Bezier curve to quadratic', (t) => {
const path = new Path2D(
'M0 10C0 4.47715 4.47715 0 10 0H90C95.5229 0 100 4.47715 100 10C100 15.5228 95.5229 20 90 20H10C4.47715 20 0 15.5228 0 10Z',
)
// Quadratic bezier curve
const quadraticPath =
'M0 10C0 4.47715 4.47715 0 10 0L90 0C95.5229 0 100 4.47715 100 10C100 15.5228 95.5229 20 90 20L10 20C4.47715 20 0 15.5228 0 10Z'

t.is(path.asWinding().simplify().toSVGString(), quadraticPath)
})

test('Convert fill-type to nonzero and cubic Bezier curve to quadratic', (t) => {
const pathTriangle = new Path2D('M70 0L0.717957 120H139.282L70 0ZM70 30L26.6987 105H113.301L70 30Z')
// Quadratic bezier curve
const quadraticPath = 'M0.717957 120L70 0L139.282 120L0.717957 120ZM113.301 105L70 30L26.6987 105L113.301 105Z'
pathTriangle.setFillType(FillType.EvenOdd)

t.is(pathTriangle.asWinding().simplify().toSVGString(), quadraticPath)
})

test('Stroke', (t) => {
const box = new Path2D()
box.rect(0, 0, 100, 100)
// Shrink effect, in which we subtract away from the original
const simplified = new Path2D(box).simplify() // sometimes required for complicated paths
const shrink = new Path2D(box).stroke({ width: 15, cap: StrokeCap.Butt }).op(simplified, PathOp.ReverseDifference)
t.is(shrink.toSVGString(), 'M7.5 92.5L7.5 7.5L92.5 7.5L92.5 92.5L7.5 92.5Z')
})

test('Convert stroke to path', (t) => {
const path = new Path2D(
'M32.9641 7L53.3157 42.25C54.8553 44.9167 52.9308 48.25 49.8516 48.25H9.14841C6.0692 48.25 4.1447 44.9167 5.6843 42.25L26.0359 7C27.5755 4.33333 31.4245 4.33333 32.9641 7Z',
)
path.stroke({ width: 10, miterLimit: 1 }).simplify().asWinding()

t.is(
path.toSVGString(),
'M57.6458 39.75L37.2942 4.5Q34.6962 -2.38419e-06 29.5 -2.38419e-06Q24.3038 -2.90573e-06 21.7058 4.5L1.35417 39.75Q-1.2439 44.25 1.35418 48.75Q3.95226 53.25 9.14841 53.25L49.8516 53.25Q55.0478 53.25 57.6458 48.75Q60.2439 44.25 57.6458 39.75ZM29.5 11L48.1195 43.25L10.8805 43.25L29.5 11Z',
)
})

test('Convert stroke to path 2', (t) => {
const path = new Path2D('M4 23.5L22.5 5L41 23.5')
path.stroke({ width: 10, join: StrokeJoin.Round, miterLimit: 1 }).simplify()

const svg = `<svg width="45" height="28" viewBox="0 0 45 28"><path fill="pink" d="${path.toSVGString()}"></path></svg>`

t.snapshot(svg)
})

test('computeTightBounds', (t) => {
const p = new Path2D()
t.deepEqual(p.computeTightBounds(), [0, 0, 0, 0])
p.arc(50, 45, 25, 0, 2 * Math.PI)
t.deepEqual(p.computeTightBounds(), [25, 20, 75, 70])
})

test('Transform', (t) => {
const p = new Path2D()
p.transform({ a: 1, b: 0.2, c: 0.8, d: 1, e: 0, f: 0 })
p.rect(0, 0, 100, 100)
p.transform({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 })
p.rect(220, 0, 100, 100)
t.is(p.toSVGString(), 'M0 0L100 0L100 100L0 100L0 0ZM220 0L320 0L320 100L220 100L220 0Z')
})

test('trim', (t) => {
const box = new Path2D()
box.rect(0, 0, 100, 100)
box.trim(0.25, 1.0)
t.snapshot(box.toSVGString())
})

function drawSimplePath() {
const path = new Path2D()
path.moveTo(0, 0)
path.lineTo(10, 0)
path.lineTo(10, 10)
path.closePath()
return path
}

test('Equals', (t) => {
const p1 = drawSimplePath()
const p2 = drawSimplePath()
t.not(p1, p2)
t.true(p1.equals(p2))
t.true(p2.equals(p1))
const blank = new Path2D()
t.false(p1.equals(blank))
t.false(p2.equals(blank))
t.false(blank.equals(p1))
t.false(blank.equals(p2))
})
17 changes: 17 additions & 0 deletions __test__/pathkit.spec.ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Snapshot report for `__test__/pathkit.spec.ts`

The actual snapshot is saved in `pathkit.spec.ts.snap`.

Generated by [AVA](https://avajs.dev).

## Convert stroke to path 2

> Snapshot 1
'<svg width="45" height="28" viewBox="0 0 45 28"><path fill="pink" d="M18.9645 1.46447L0.464466 19.9645L7.53553 27.0355L22.5 12.0711L37.4645 27.0355L44.5355 19.9645L26.0355 1.46447Q25.9487 1.37767 25.8578 1.29524Q25.7668 1.21282 25.672 1.13495Q25.5771 1.05708 25.4785 0.983962Q25.3799 0.910844 25.2778 0.842652Q25.1758 0.774459 25.0705 0.711357Q24.9652 0.648254 24.857 0.590394Q24.7487 0.532533 24.6378 0.480054Q24.5268 0.427574 24.4134 0.380602Q24.3 0.333631 24.1844 0.29228Q24.0689 0.250929 23.9514 0.215298Q23.834 0.179668 23.7149 0.149844Q23.5958 0.12002 23.4755 0.0960736Q23.3551 0.0721276 23.2337 0.0541174Q23.1122 0.0361073 22.9901 0.0240764Q22.8679 0.0120454 22.7453 0.00602272Q22.6227 0 22.5 0Q22.3773 0 22.2547 0.00602272Q22.1321 0.0120454 22.0099 0.0240764Q21.8878 0.0361073 21.7663 0.0541174Q21.6449 0.0721276 21.5245 0.0960736Q21.4042 0.12002 21.2851 0.149844Q21.166 0.179668 21.0486 0.215298Q20.9311 0.250929 20.8155 0.29228Q20.7 0.333631 20.5866 0.380602Q20.4732 0.427574 20.3622 0.480054Q20.2513 0.532533 20.143 0.590394Q20.0348 0.648254 19.9295 0.711357Q19.8242 0.774459 19.7221 0.842652Q19.6201 0.910844 19.5215 0.983962Q19.4229 1.05708 19.328 1.13495Q19.2332 1.21282 19.1422 1.29524Q19.0513 1.37767 18.9645 1.46447Z"></path></svg>'

## trim

> Snapshot 1
'M100 0L100 0L100 100L0 100L0 0'
Binary file added __test__/pathkit.spec.ts.snap
Binary file not shown.
48 changes: 48 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,27 @@ export class Path2D {
moveTo(x: number, y: number): void
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
rect(x: number, y: number, w: number, h: number): void

// PathKit methods
op(path: Path2D, operation: PathOp): Path2D
toSVGString(): string
getFillType(): FillType
setFillType(type: FillType): void
simplify(): Path2D
asWinding(): Path2D
stroke(stroke?: StrokeOptions): Path2D
transform(transform: DOMMatrix2DInit): Path2D
getBounds(): [left: number, top: number, right: number, bottom: number]
computeTightBounds(): [left: number, top: number, right: number, bottom: number]
trim(start: number, end: number, isComplement?: boolean): Path2D
equals(path: Path2D): boolean
}

export interface StrokeOptions {
width?: number
miterLimit?: number
cap?: StrokeCap
join?: StrokeJoin
}

export interface SKRSContext2D extends Omit<CanvasRenderingContext2D, 'drawImage' | 'createPattern'> {
Expand Down Expand Up @@ -242,3 +263,30 @@ export interface Canvas extends Omit<HTMLCanvasElement, 'getContext'> {
}

export function createCanvas(width: number, height: number): Canvas

export const enum PathOp {
Difference = 0, // subtract the op path from the first path
Intersect = 1, // intersect the two paths
Union = 2, // union (inclusive-or) the two paths
XOR = 3, // exclusive-or the two paths
ReverseDifference = 4, // subtract the first path from the op path
}

export const enum FillType {
Winding = 0,
EvenOdd = 1,
InverseWinding = 2,
InverseEvenOdd = 3,
}

export const enum StrokeJoin {
Miter = 0,
Round = 1,
Bevel = 2,
}

export const enum StrokeCap {
Butt = 0,
Round = 1,
Square = 2,
}
40 changes: 40 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ const { CanvasRenderingContext2D, CanvasElement, Path2D, ImageData, Image, Canva

const Geometry = require('./geometry')

const StrokeJoin = {
Miter: 0,
Round: 1,
Bevel: 2,
}

const StrokeCap = {
Butt: 0,
Round: 1,
Square: 2,
}

CanvasRenderingContext2D.prototype.createPattern = function createPattern(image, repetition) {
if (image instanceof ImageData) {
const pattern = new CanvasPattern(image, repetition, 0)
Expand All @@ -37,6 +49,15 @@ CanvasRenderingContext2D.prototype.getImageData = function getImageData(x, y, w,
return new ImageData(data, w, h)
}

Path2D.prototype.stroke = function stroke(strokeOptions = {}) {
const width = typeof strokeOptions.width === 'undefined' ? 1 : strokeOptions.width
const miterLimit = typeof strokeOptions.miterLimit === 'undefined' ? 1 : strokeOptions.miterLimit
const join = typeof strokeOptions.join === 'undefined' ? StrokeJoin.Miter : strokeOptions.join
const cap = typeof strokeOptions.cap === 'undefined' ? StrokeCap.Butt : strokeOptions.cap

return this._stroke(width, miterLimit, join, cap)
}

function createCanvas(width, height) {
const canvasElement = new CanvasElement(width, height)
const ctx = new CanvasRenderingContext2D(width, height)
Expand Down Expand Up @@ -79,10 +100,29 @@ function createCanvas(width, height) {
return canvasElement
}

const PathOp = {
Difference: 0, // subtract the op path from the first path
Intersect: 1, // intersect the two paths
Union: 2, // union (inclusive-or) the two paths
XOR: 3, // exclusive-or the two paths
ReverseDifference: 4, // subtract the first path from the op path
}

const FillType = {
Winding: 0,
EvenOdd: 1,
InverseWinding: 2,
InverseEvenOdd: 3,
}

module.exports = {
createCanvas,
Path2D,
ImageData,
Image,
PathOp,
FillType,
StrokeCap,
StrokeJoin,
...Geometry,
}
14 changes: 4 additions & 10 deletions npm/android-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
{
"name": "@napi-rs/canvas-android-arm64",
"version": "0.0.3",
"os": [
"android"
],
"cpu": [
"arm64"
],
"os": ["android"],
"cpu": ["arm64"],
"main": "skia.android-arm64.node",
"files": [
"skia.android-arm64.node"
],
"files": ["skia.android-arm64.node"],
"description": "Canvas for Node.js with skia backend",
"keywords": [
"napi-rs",
Expand All @@ -34,4 +28,4 @@
"access": "public"
},
"repository": "https://github.com/Brooooooklyn/canvas.git"
}
}
14 changes: 4 additions & 10 deletions npm/darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
{
"name": "@napi-rs/canvas-darwin-arm64",
"version": "0.0.3",
"os": [
"darwin"
],
"cpu": [
"arm64"
],
"os": ["darwin"],
"cpu": ["arm64"],
"main": "skia.darwin-arm64.node",
"files": [
"skia.darwin-arm64.node"
],
"files": ["skia.darwin-arm64.node"],
"description": "Canvas for Node.js with skia backend",
"keywords": [
"napi-rs",
Expand All @@ -34,4 +28,4 @@
"access": "public"
},
"repository": "https://github.com/Brooooooklyn/canvas.git"
}
}
Loading

1 comment on commit eea95bf

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: eea95bf Previous: 67f4443 Ratio
Draw house#@napi-rs/skia 25 ops/sec (±1.42%) 25 ops/sec (±3.34%) 1
Draw house#node-canvas 21 ops/sec (±2.52%) 23 ops/sec (±4.13%) 1.10
Draw gradient#@napi-rs/skia 25 ops/sec (±1.18%) 25 ops/sec (±2.4%) 1
Draw gradient#node-canvas 21 ops/sec (±2.3%) 24 ops/sec (±2.57%) 1.14

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.