Skip to content

Commit 8e112d2

Browse files
committed
add roundRect() method to context and Path2D
1 parent 2f38a32 commit 8e112d2

File tree

10 files changed

+169
-23
lines changed

10 files changed

+169
-23
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
### New Features
66
- The new [Window][window] class can display a **Canvas** on screen, respond to mouse and keyboard input, and fluidly [animate][window_anim] by calling user-defined [event handlers][window_events].
77
- Bitmap rendering now occurs on the GPU by default and can be configured using the **Canvas**'s [`.gpu`][canvas_gpu] property. If the platform supports hardware-accelerated rendering (using Metal on macOS and Vulkan on Linux & Windows), the property will be `true` by default and can be set to `false` to use the software renderer.
8-
- Added support for Chrome’s [`reset()`][chrome_reset] context method which erases the canvas, resets the transformation state, and clears the current path
8+
- Added support for recent Chrome features:
9+
- the [`reset()`][chrome_reset] context method which erases the canvas, resets the transformation state, and clears the current path
10+
- the [`roundRect()`][chrome_rrect] method on contexts and **Path2D** objects which adds a rounded rectangle using 1–4 corner radii (provided as a single value or an array of numbers and/or **DOMPoint** objects)
911

1012
### Bugfixes
1113
- The `FontLibrary.reset()` method didn't actually remove previously installed fonts that had already been drawn with (and thus cached). It now clears those caches, which also means previously used fonts can now be replaced by calling `.use()` again with the same family name.
@@ -14,7 +16,7 @@
1416
### Misc. Improvements
1517
- The [`.filter`][filter] property's `"blur(…)"` and `"drop-shadow(…)"` effects now match browser behavior much more closely and scale appropriately with the `density` export option.
1618
- Antialiasing is smoother, particularly when down-scaling images, thanks to the use of mipmaps rather than Skia's (apparently buggy?) implementation of bucubic interpolation.
17-
- Calling `clearRect()` with dimensions that fully enclose the canvas will now discard all the vector objects that have been drawn so far (rather than simply covering them up). Assigning a new `width` or `height` to the canvas will have a similar effect.
19+
- Calling `clearRect()` with dimensions that fully enclose the canvas will now discard all the vector objects that have been drawn so far (rather than simply covering them up).
1820
- Upgraded Skia to milestone 103
1921

2022
[window]: https://github.com/samizdatco/skia-canvas#window
@@ -23,6 +25,7 @@
2325
[canvas_gpu]: https://github.com/samizdatco/skia-canvas#gpu
2426
[filter]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
2527
[chrome_reset]: https://developer.chrome.com/blog/canvas2d/#context-reset
28+
[chrome_rrect]: https://developer.chrome.com/blog/canvas2d/#round-rect
2629

2730
## 📦 ⟩ [v0.9.30] ⟩ Jun 7, 2022
2831

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,14 @@ Most of your interaction with the canvas will actually be directed toward its
338338
|-----------------------------------------------|---------------------------------------------------|---------------------------------------------------|----------------------------------------------|--------------------------------------------------|------------------------------------------|------------------------------------------------------------------|----------------------------------------------------|----------------------------------------------------------|
339339
| [**canvas**][canvas_attr] ⧸[](#canvas) | [clearRect()][clearRect()] | [**fillStyle**][fillStyle] | [**lineCap**][lineCap] | [**currentTransform**][currentTransform] | [moveTo()][moveTo()] | [**direction**][direction] | [**imageSmoothingEnabled**][imageSmoothingEnabled] | [**filter**][filter] |
340340
| [beginPath()][beginPath()] | [fillRect()][fillRect()] | [**strokeStyle**][strokeStyle] | [**lineDashFit** ⚡][lineDashFit] | [createProjection() ⚡][createProjection()] | [lineTo()][lineTo()] | [**font**][font] ⧸[](#font) | [**imageSmoothingQuality**][imageSmoothingQuality] | [**globalAlpha**][globalAlpha] |
341-
| [isPointInPath()][isPointInPath()] | [strokeRect()][strokeRect()] | [createConicGradient()][createConicGradient()] | [**lineDashMarker** ⚡][lineDashMarker] | [getTransform()][getTransform()] | [arcTo()][arcTo()] | [**fontVariant** ⚡](#fontvariant) | [createImageData()][createImageData()] | [**globalCompositeOperation**][globalCompositeOperation] |
342-
| [isPointInStroke()][isPointInStroke()] | [fillText()][fillText()] ⧸[][drawText] | [createLinearGradient()][createLinearGradient()] | [**lineDashOffset**][lineDashOffset] | [setTransform()][setTransform()] | [bezierCurveTo()][bezierCurveTo()] | [**textAlign**][textAlign] | [getImageData()][getImageData()] | [**shadowBlur**][shadowBlur] |
343-
| [save()][save()] | [strokeText()][strokeText()] ⧸[][drawText] | [createRadialGradient()][createRadialGradient()] | [**lineJoin**][lineJoin] | [resetTransform()][resetTransform()] | [conicCurveTo() ⚡][conicCurveTo] | [**textBaseline**][textBaseline] | [putImageData()][putImageData()] | [**shadowColor**][shadowColor] |
344-
| [restore()][restore()] | [fill()][fill()] | [createPattern()][createPattern()] | [**lineWidth**][lineWidth] | [transform()][transform()] | [quadraticCurveTo()][quadraticCurveTo()] | [**textTracking** ⚡](#texttracking) | [drawCanvas() ⚡](#drawcanvascanvas-x-y-) | [**shadowOffsetX**][shadowOffsetX] |
345-
| [reset()][reset()] | [stroke()][stroke()] | [createTexture() ⚡][createTexture()] | [**miterLimit**][miterLimit] | [translate()][translate()] | [closePath()][closePath()] | [**textWrap** ⚡](#textwrap) | [drawImage()][drawImage()] | [**shadowOffsetY**][shadowOffsetY] |
346-
| [clip()][clip()] | | | [getLineDash()][getLineDash()] | [rotate()][rotate()] | [arc()][arc()] | [measureText()][measureText()] ⧸[](#measuretextstr-width) | | |
347-
| | | | [setLineDash()][setLineDash()] | [scale()][scale()] | [ellipse()][ellipse()] | [outlineText() ⚡][outlineText()] | | |
348-
| | | | | | [rect()][rect()] | | |
341+
| [closePath()][closePath()] | [strokeRect()][strokeRect()] | [createConicGradient()][createConicGradient()] | [**lineDashMarker** ⚡][lineDashMarker] | [getTransform()][getTransform()] | [arcTo()][arcTo()] | [**fontVariant** ⚡](#fontvariant) | [createImageData()][createImageData()] | [**globalCompositeOperation**][globalCompositeOperation] |
342+
| [isPointInPath()][isPointInPath()] | [fillText()][fillText()] ⧸[][drawText] | [createLinearGradient()][createLinearGradient()] | [**lineDashOffset**][lineDashOffset] | [setTransform()][setTransform()] | [bezierCurveTo()][bezierCurveTo()] | [**textAlign**][textAlign] | [getImageData()][getImageData()] | [**shadowBlur**][shadowBlur] |
343+
| [isPointInStroke()][isPointInStroke()] | [strokeText()][strokeText()] ⧸[][drawText] | [createRadialGradient()][createRadialGradient()] | [**lineJoin**][lineJoin] | [resetTransform()][resetTransform()] | [conicCurveTo() ⚡][conicCurveTo] | [**textBaseline**][textBaseline] | [putImageData()][putImageData()] | [**shadowColor**][shadowColor] |
344+
| [save()][save()] | [fill()][fill()] | [createPattern()][createPattern()] | [**lineWidth**][lineWidth] | [transform()][transform()] | [quadraticCurveTo()][quadraticCurveTo()] | [**textTracking** ⚡](#texttracking) | [drawCanvas() ⚡](#drawcanvascanvas-x-y-) | [**shadowOffsetX**][shadowOffsetX] |
345+
| [restore()][restore()] | [stroke()][stroke()] | [createTexture() ⚡][createTexture()] | [**miterLimit**][miterLimit] | [translate()][translate()] | [arc()][arc()] | [**textWrap** ⚡](#textwrap) | [drawImage()][drawImage()] | [**shadowOffsetY**][shadowOffsetY] |
346+
| [reset()][reset()] | | | [getLineDash()][getLineDash()] | [rotate()][rotate()] | [ellipse()][ellipse()] | [measureText()][measureText()] ⧸[](#measuretextstr-width) | | |
347+
| [clip()][clip()] | | | [setLineDash()][setLineDash()] | [scale()][scale()] | [rect()][rect()] | [outlineText() ⚡][outlineText()] | | |
348+
| | | | | | [roundRect()][roundRect()] | | |
349349

350350
##### PROPERTIES
351351

@@ -587,14 +587,14 @@ for (let i=0; i<8000; i++){
587587
The `Path2D` class allows you to create paths independent of a given [Canvas](#canvas) or [graphics context](#canvasrenderingcontext2d). These paths can be modified over time and drawn repeatedly (potentially on multiple canvases). `Path2D` objects can also be used as [lineDashMarker][lineDashMarker]s or as the repeating pattern in a [CanvasTexture][createTexture()].
588588

589589

590-
| Line Segments | Shapes | Boolean Ops ⚡ | Filters ⚡ | Geometry ⚡ |
591-
| -- | -- | -- | -- | -- |
592-
| [**d**](#d) | [addPath()][p2d_addPath] | [complement()][bool-ops] | [interpolate()][p2d_interpolate] | [**bounds**](#bounds) |
593-
| [moveTo()][p2d_moveTo] | [arc()][p2d_arc] | [difference()][bool-ops] | [jitter()][p2d_jitter] | [**edges**](#edges) |
594-
| [lineTo()][p2d_lineTo] | [arcTo()][p2d_arcTo] | [intersect()][bool-ops] | [round()][p2d_round] | [contains()][p2d_contains] |
595-
| [bezierCurveTo()][p2d_bezierCurveTo] | [ellipse()][p2d_ellipse] | [union()][bool-ops] | [simplify()][p2d_simplify] | [points()][p2d_points] |
596-
| [conicCurveTo() ⚡][conicCurveTo] | [rect()][p2d_rect] | [xor()][bool-ops] | [trim()][p2d_trim] | [offset()][p2d_offset] |
597-
| [quadraticCurveTo()][p2d_quadraticCurveTo] | | | [unwind()][p2d_unwind] | [transform()][p2d_transform] |
590+
| Line Segments | Shapes | Boolean Ops ⚡ | Filters ⚡ | Geometry ⚡ |
591+
| -- | -- | -- | -- | -- |
592+
| [**d**](#d) | [addPath()][p2d_addPath] | [complement()][bool-ops] | [interpolate()][p2d_interpolate] | [**bounds**](#bounds) |
593+
| [moveTo()][p2d_moveTo] | [arc()][p2d_arc] | [difference()][bool-ops] | [jitter()][p2d_jitter] | [**edges**](#edges) |
594+
| [lineTo()][p2d_lineTo] | [arcTo()][p2d_arcTo] | [intersect()][bool-ops] | [round()][p2d_round] | [contains()][p2d_contains] |
595+
| [bezierCurveTo()][p2d_bezierCurveTo] | [ellipse()][p2d_ellipse] | [union()][bool-ops] | [simplify()][p2d_simplify] | [points()][p2d_points] |
596+
| [conicCurveTo() ⚡][conicCurveTo] | [rect()][p2d_rect] | [xor()][bool-ops] | [trim()][p2d_trim] | [offset()][p2d_offset] |
597+
| [quadraticCurveTo()][p2d_quadraticCurveTo] | [roundRect()][roundRect()] | | [unwind()][p2d_unwind] | [transform()][p2d_transform] |
598598
| [closePath()][p2d_closePath] |
599599

600600
#### Creating `Path2D` objects
@@ -1362,6 +1362,7 @@ Many thanks to the [`node-canvas`](https://github.com/Automattic/node-canvas) de
13621362
[transform()]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform
13631363
[translate()]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/translate
13641364
[reset()]: https://developer.chrome.com/blog/canvas2d/#context-reset
1365+
[roundRect()]: https://developer.chrome.com/blog/canvas2d/#round-rect
13651366
13661367
[nonzero]: https://en.wikipedia.org/wiki/Nonzero-rule
13671368
[evenodd]: https://en.wikipedia.org/wiki/Even–odd_rule

lib/css.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// https://www.w3.org/TR/css-fonts-3/#font-size-prop
1010

1111
var splitBy = require('string-split-by'),
12+
{DOMPoint} = require('./geometry'),
1213
m, cache = {font:{}, variant:{}};
1314

1415
const styleRE = /^(normal|italic|oblique)$/,
@@ -173,6 +174,27 @@ function parseFit(mode){
173174
return ["none", "contain-x", "contain-y", "contain", "cover", "fill", "scale-down", "resize"].includes(mode)
174175
}
175176

177+
// -- Corner Rounding
178+
// https://github.com/fserb/canvas2D/blob/master/spec/roundrect.md
179+
180+
function parseCornerRadii(r){
181+
r = [r].flat()
182+
.map(n => n instanceof DOMPoint ? n : new DOMPoint(n, n))
183+
.slice(0, 4)
184+
185+
if (r.some(pt => !Number.isFinite(pt.x) || !Number.isFinite(pt.y))){
186+
return null // silently abort
187+
}else if (r.some(pt => pt.x < 0 || pt.y < 0)){
188+
throw new Error("Corner radius cannot be negative")
189+
}
190+
191+
return r.length == 1 ? [r[0], r[0], r[0], r[0]]
192+
: r.length == 2 ? [r[0], r[1], r[0], r[1]]
193+
: r.length == 3 ? [r[0], r[1], r[2], r[1]]
194+
: r.length == 4 ? [r[0], r[1], r[2], r[3]]
195+
: [0, 0, 0, 0].map(n => new DOMPoint(n, n))
196+
}
197+
176198
// -- Image Filters -----------------------------------------------------------------------
177199
// https://developer.mozilla.org/en-US/docs/Web/CSS/filter
178200

@@ -318,4 +340,5 @@ module.exports = {
318340
filter:parseFilter,
319341
cursor:parseCursor,
320342
fit:parseFit,
343+
radii:parseCornerRadii,
321344
}

lib/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ interface CanvasFillStrokeStyles {
120120
type QuadOrRect = [x1:number, y1:number, x2:number, y2:number, x3:number, y3:number, x4:number, y4:number] |
121121
[left:number, top:number, right:number, bottom:number] | [width:number, height:number]
122122

123+
type CornerRadius = number | DOMPoint
124+
123125
export interface CanvasRenderingContext2D extends CanvasCompositing, CanvasDrawImage, CanvasDrawPath, CanvasFillStrokeStyles, CanvasFilters, CanvasImageData, CanvasImageSmoothing, CanvasPath, CanvasPathDrawingStyles, CanvasRect, CanvasShadowStyles, CanvasState, CanvasText, CanvasTextDrawingStyles, CanvasTransform, CanvasUserInterface {
124126
readonly canvas: Canvas;
125127
fontVariant: string;
@@ -133,6 +135,7 @@ export interface CanvasRenderingContext2D extends CanvasCompositing, CanvasDrawI
133135
createProjection(quad: QuadOrRect, basis?: QuadOrRect): DOMMatrix
134136

135137
conicCurveTo(cpx: number, cpy: number, x: number, y: number, weight: number): void
138+
roundRect(x: number, y: number, width: number, height: number, radii: number | CornerRadius[])
136139
// getContextAttributes(): CanvasRenderingContext2DSettings;
137140

138141
fillText(text: string, x: number, y:number, maxWidth?: number): void
@@ -172,6 +175,8 @@ export class Path2D extends globalThis.Path2D {
172175
weight: number
173176
): void
174177

178+
roundRect(x: number, y: number, width: number, height: number, radii: number | CornerRadius[])
179+
175180
complement(otherPath: Path2D): Path2D
176181
difference(otherPath: Path2D): Path2D
177182
intersect(otherPath: Path2D): Path2D

lib/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,16 @@ class CanvasRenderingContext2D extends RustClass{
607607
isPointInPath(x, y){ return this.ƒ('isPointInPath', ...arguments) }
608608
isPointInStroke(x, y){ return this.ƒ('isPointInStroke', ...arguments) }
609609

610+
roundRect(x, y, w, h, r){
611+
let radii = css.radii(r)
612+
if (radii){
613+
if (w < 0) radii = [radii[1], radii[0], radii[3], radii[2]]
614+
if (h < 0) radii = [radii[3], radii[2], radii[1], radii[0]]
615+
this.ƒ("roundRect", x, y, w, h, ...radii.map(({x, y}) => [x, y]).flat())
616+
}
617+
}
618+
619+
610620
// -- using paths -----------------------------------------------------------
611621
fill(path, rule){
612622
if (path instanceof Path2D) this.ƒ('fill', core(path), rule)
@@ -975,6 +985,14 @@ class Path2D extends RustClass{
975985
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, isCCW){ this.ƒ("ellipse", ...arguments) }
976986
rect(x, y, width, height){this.ƒ("rect", ...arguments) }
977987
arc(x, y, radius, startAngle, endAngle){ this.ƒ("arc", ...arguments) }
988+
roundRect(x, y, w, h, r){
989+
let radii = css.radii(r)
990+
if (radii){
991+
if (w < 0) radii = [radii[1], radii[0], radii[3], radii[2]]
992+
if (h < 0) radii = [radii[3], radii[2], radii[1], radii[0]]
993+
this.ƒ("roundRect", x, y, w, h, ...radii.map(({x, y}) => [x, y]).flat())
994+
}
995+
}
978996

979997
// tween similar paths
980998
interpolate(path, weight){ return Path2D.interpolate(this, path, weight) }

0 commit comments

Comments
 (0)