Skip to content

Commit dce0fd1

Browse files
committed
Add roundRect() support
https://developer.chrome.com/blog/canvas2d/#round-rect WPT tests: 326 passing (1s) 9 pending 129 failing (down from 179)
1 parent eba1e4a commit dce0fd1

File tree

6 files changed

+218
-3
lines changed

6 files changed

+218
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
1010
### Changed
1111
* Export `pangoVersion`
1212
### Added
13+
* [`ctx.roundRect()`](https://developer.chrome.com/blog/canvas2d/#round-rect)
1314
### Fixed
1415
* `rgba(r,g,b)` with no alpha should parse as opaque, not transparent. ([#2029](https://github.com/Automattic/node-canvas/issues/2029))
1516
* Typo in `PngConfig.filters` types. ([#2072](https://github.com/Automattic/node-canvas/issues/2072))

binding.gyp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
'<(GTK_Root)/lib/glib-2.0/include'
9797
],
9898
'defines': [
99-
'_USE_MATH_DEFINES' # for M_PI
99+
'_USE_MATH_DEFINES', # for M_PI
100+
'NOMINMAX' # allow std::min/max to work
100101
],
101102
'configurations': {
102103
'Debug': {

src/CanvasRenderingContext2d.cc

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Context2d::Initialize(Nan::ADDON_REGISTER_FUNCTION_ARGS_TYPE target) {
126126
Nan::SetPrototypeMethod(ctor, "strokeRect", StrokeRect);
127127
Nan::SetPrototypeMethod(ctor, "clearRect", ClearRect);
128128
Nan::SetPrototypeMethod(ctor, "rect", Rect);
129+
Nan::SetPrototypeMethod(ctor, "roundRect", RoundRect);
129130
Nan::SetPrototypeMethod(ctor, "measureText", MeasureText);
130131
Nan::SetPrototypeMethod(ctor, "moveTo", MoveTo);
131132
Nan::SetPrototypeMethod(ctor, "lineTo", LineTo);
@@ -2937,6 +2938,179 @@ NAN_METHOD(Context2d::Rect) {
29372938
}
29382939
}
29392940

2941+
// Draws an arc with two potentially different radii.
2942+
inline static
2943+
void elli_arc(cairo_t* ctx, double xc, double yc, double rx, double ry, double a1, double a2, bool clockwise=true) {
2944+
if (rx == 0. || ry == 0.) {
2945+
cairo_line_to(ctx, xc + rx, yc + ry);
2946+
} else {
2947+
cairo_save(ctx);
2948+
cairo_translate(ctx, xc, yc);
2949+
cairo_scale(ctx, rx, ry);
2950+
if (clockwise)
2951+
cairo_arc(ctx, 0., 0., 1., a1, a2);
2952+
else
2953+
cairo_arc_negative(ctx, 0., 0., 1., a2, a1);
2954+
cairo_restore(ctx);
2955+
}
2956+
}
2957+
2958+
inline static
2959+
bool getRadius(Point<double>& p, const Local<Value>& v) {
2960+
if (v->IsObject()) { // 5.1 DOMPointInit
2961+
auto rx = Nan::Get(v.As<Object>(), Nan::New("x").ToLocalChecked()).ToLocalChecked();
2962+
auto ry = Nan::Get(v.As<Object>(), Nan::New("y").ToLocalChecked()).ToLocalChecked();
2963+
if (rx->IsNumber() && ry->IsNumber()) {
2964+
auto rxv = Nan::To<double>(rx).FromJust();
2965+
auto ryv = Nan::To<double>(ry).FromJust();
2966+
if (!std::isfinite(rxv) || !std::isfinite(ryv))
2967+
return true;
2968+
if (rxv < 0 || ryv < 0) {
2969+
Nan::ThrowRangeError("radii must be positive.");
2970+
return true;
2971+
}
2972+
p.x = rxv;
2973+
p.y = ryv;
2974+
return false;
2975+
}
2976+
} else if (v->IsNumber()) { // 5.2 unrestricted double
2977+
auto rv = Nan::To<double>(v).FromJust();
2978+
if (!std::isfinite(rv))
2979+
return true;
2980+
if (rv < 0) {
2981+
Nan::ThrowRangeError("radii must be positive.");
2982+
return true;
2983+
}
2984+
p.x = p.y = rv;
2985+
return false;
2986+
}
2987+
return true;
2988+
}
2989+
2990+
/**
2991+
* https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect
2992+
* x, y, w, h, [radius|[radii]]
2993+
*/
2994+
NAN_METHOD(Context2d::RoundRect) {
2995+
RECT_ARGS;
2996+
Context2d *context = Nan::ObjectWrap::Unwrap<Context2d>(info.This());
2997+
cairo_t *ctx = context->context();
2998+
2999+
// 4. Let normalizedRadii be an empty list
3000+
Point<double> normalizedRadii[4];
3001+
size_t nRadii = 4;
3002+
3003+
if (info[4]->IsUndefined()) {
3004+
for (size_t i = 0; i < 4; i++)
3005+
normalizedRadii[i].x = normalizedRadii[i].y = 0.;
3006+
3007+
} else if (info[4]->IsArray()) {
3008+
auto radiiList = info[4].As<v8::Array>();
3009+
nRadii = radiiList->Length();
3010+
if (!(nRadii >= 1 && nRadii <= 4)) {
3011+
Nan::ThrowRangeError("radii must be a list of one, two, three or four radii.");
3012+
return;
3013+
}
3014+
// 5. For each radius of radii
3015+
for (size_t i = 0; i < nRadii; i++) {
3016+
auto r = Nan::Get(radiiList, i).ToLocalChecked();
3017+
if (getRadius(normalizedRadii[i], r))
3018+
return;
3019+
}
3020+
3021+
} else {
3022+
// 2. If radii is a double, then set radii to <<radii>>
3023+
if (getRadius(normalizedRadii[0], info[4]))
3024+
return;
3025+
for (size_t i = 1; i < 4; i++) {
3026+
normalizedRadii[i].x = normalizedRadii[0].x;
3027+
normalizedRadii[i].y = normalizedRadii[0].y;
3028+
}
3029+
}
3030+
3031+
Point<double> upperLeft, upperRight, lowerRight, lowerLeft;
3032+
if (nRadii == 4) {
3033+
upperLeft = normalizedRadii[0];
3034+
upperRight = normalizedRadii[1];
3035+
lowerRight = normalizedRadii[2];
3036+
lowerLeft = normalizedRadii[3];
3037+
} else if (nRadii == 3) {
3038+
upperLeft = normalizedRadii[0];
3039+
upperRight = normalizedRadii[1];
3040+
lowerLeft = normalizedRadii[1];
3041+
lowerRight = normalizedRadii[2];
3042+
} else if (nRadii == 2) {
3043+
upperLeft = normalizedRadii[0];
3044+
lowerRight = normalizedRadii[0];
3045+
upperRight = normalizedRadii[1];
3046+
lowerLeft = normalizedRadii[1];
3047+
} else {
3048+
upperLeft = normalizedRadii[0];
3049+
upperRight = normalizedRadii[0];
3050+
lowerRight = normalizedRadii[0];
3051+
lowerLeft = normalizedRadii[0];
3052+
}
3053+
3054+
bool clockwise = true;
3055+
if (width < 0) {
3056+
clockwise = false;
3057+
x += width;
3058+
width = -width;
3059+
std::swap(upperLeft, upperRight);
3060+
std::swap(lowerLeft, lowerRight);
3061+
}
3062+
3063+
if (height < 0) {
3064+
clockwise = !clockwise;
3065+
y += height;
3066+
height = -height;
3067+
std::swap(upperLeft, lowerLeft);
3068+
std::swap(upperRight, lowerRight);
3069+
}
3070+
3071+
// 11. Corner curves must not overlap. Scale radii to prevent this.
3072+
{
3073+
auto top = upperLeft.x + upperRight.x;
3074+
auto right = upperRight.y + lowerRight.y;
3075+
auto bottom = lowerRight.x + lowerLeft.x;
3076+
auto left = upperLeft.y + lowerLeft.y;
3077+
auto scale = std::min({ width / top, height / right, width / bottom, height / left });
3078+
if (scale < 1.) {
3079+
upperLeft.x *= scale;
3080+
upperLeft.y *= scale;
3081+
upperRight.x *= scale;
3082+
upperRight.x *= scale;
3083+
lowerLeft.y *= scale;
3084+
lowerLeft.y *= scale;
3085+
lowerRight.y *= scale;
3086+
lowerRight.y *= scale;
3087+
}
3088+
}
3089+
3090+
// 12. Draw
3091+
cairo_move_to(ctx, x + upperLeft.x, y);
3092+
if (clockwise) {
3093+
cairo_line_to(ctx, x + width - upperRight.x, y);
3094+
elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0.);
3095+
cairo_line_to(ctx, x + width, y + height - lowerRight.y);
3096+
elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2.);
3097+
cairo_line_to(ctx, x + lowerLeft.x, y + height);
3098+
elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI);
3099+
cairo_line_to(ctx, x, y + upperLeft.y);
3100+
elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2.);
3101+
} else {
3102+
elli_arc(ctx, x + upperLeft.x, y + upperLeft.y, upperLeft.x, upperLeft.y, M_PI, 3. * M_PI / 2., false);
3103+
cairo_line_to(ctx, x, y + upperLeft.y);
3104+
elli_arc(ctx, x + lowerLeft.x, y + height - lowerLeft.y, lowerLeft.x, lowerLeft.y, M_PI / 2., M_PI, false);
3105+
cairo_line_to(ctx, x + lowerLeft.x, y + height);
3106+
elli_arc(ctx, x + width - lowerRight.x, y + height - lowerRight.y, lowerRight.x, lowerRight.y, 0, M_PI / 2., false);
3107+
cairo_line_to(ctx, x + width, y + height - lowerRight.y);
3108+
elli_arc(ctx, x + width - upperRight.x, y + upperRight.y, upperRight.x, upperRight.y, 3. * M_PI / 2., 0., false);
3109+
cairo_line_to(ctx, x + width - upperRight.x, y);
3110+
}
3111+
cairo_close_path(ctx);
3112+
}
3113+
29403114
// Adapted from https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/modules/canvas2d/CanvasPathMethods.cpp
29413115
static void canonicalizeAngle(double& startAngle, double& endAngle) {
29423116
// Make 0 <= startAngle < 2*PI

src/CanvasRenderingContext2d.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class Context2d: public Nan::ObjectWrap {
104104
static NAN_METHOD(StrokeRect);
105105
static NAN_METHOD(ClearRect);
106106
static NAN_METHOD(Rect);
107+
static NAN_METHOD(RoundRect);
107108
static NAN_METHOD(Arc);
108109
static NAN_METHOD(ArcTo);
109110
static NAN_METHOD(Ellipse);

src/Point.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
#pragma once
44

5-
template <class T>
5+
template <typename T>
66
class Point {
77
public:
88
T x, y;
9-
Point(T x, T y): x(x), y(y) {}
9+
Point(T x=0, T y=0): x(x), y(y) {}
10+
Point(const Point&) = default;
1011
};

test/public/tests.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,43 @@ tests['fillRect()'] = function (ctx) {
9595
render(1)
9696
}
9797

98+
tests['roundRect()'] = function (ctx) {
99+
if (!ctx.roundRect) {
100+
ctx.textAlign = 'center'
101+
ctx.fillText('roundRect() not supported', 100, 100, 190)
102+
ctx.fillText('try Chrome instead', 100, 115, 190)
103+
return
104+
}
105+
ctx.roundRect(5, 5, 60, 60, 20)
106+
ctx.fillStyle = 'red'
107+
ctx.fill()
108+
109+
ctx.beginPath()
110+
ctx.roundRect(5, 70, 60, 60, [10, 15, 20, 25])
111+
ctx.fillStyle = 'blue'
112+
ctx.fill()
113+
114+
ctx.beginPath()
115+
ctx.roundRect(70, 5, 60, 60, [10])
116+
ctx.fillStyle = 'green'
117+
ctx.fill()
118+
119+
ctx.beginPath()
120+
ctx.roundRect(70, 70, 60, 60, [10, 15])
121+
ctx.fillStyle = 'orange'
122+
ctx.fill()
123+
124+
ctx.beginPath()
125+
ctx.roundRect(135, 5, 60, 60, [10, 15, 20])
126+
ctx.fillStyle = 'pink'
127+
ctx.fill()
128+
129+
ctx.beginPath()
130+
ctx.roundRect(135, 70, 60, 60, [{ x: 30, y: 10 }, { x: 5, y: 20 }])
131+
ctx.fillStyle = 'darkseagreen'
132+
ctx.fill()
133+
}
134+
98135
tests['lineTo()'] = function (ctx) {
99136
// Filled triangle
100137
ctx.beginPath()

0 commit comments

Comments
 (0)