Skip to content

Commit

Permalink
Enhance the rounded rectangle implementation (#5597)
Browse files Browse the repository at this point in the history
Use `arcTo` instead of `quadraticCurveTo` (both methods have the same compatibility level) because it generates better results when the final rect is a circle but also when it's actually a rectangle and not a square. This change is needed by the datalabels plugin where the user can configure the `borderRadius` and thus generate circle from a rounded rectangle.
  • Loading branch information
simonbrunel committed Jun 26, 2018
1 parent 6f90e07 commit 88308c6
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 18 deletions.
34 changes: 21 additions & 13 deletions src/helpers/helpers.canvas.js
Expand Up @@ -27,18 +27,20 @@ var exports = module.exports = {
*/
roundedRect: function(ctx, x, y, width, height, radius) {
if (radius) {
var rx = Math.min(radius, width / 2);
var ry = Math.min(radius, height / 2);

ctx.moveTo(x + rx, y);
ctx.lineTo(x + width - rx, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + ry);
ctx.lineTo(x + width, y + height - ry);
ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height);
ctx.lineTo(x + rx, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - ry);
ctx.lineTo(x, y + ry);
ctx.quadraticCurveTo(x, y, x + rx, y);
// NOTE(SB) `epsilon` helps to prevent minor artifacts appearing
// on Chrome when `r` is exactly half the height or the width.
var epsilon = 0.0000001;
var r = Math.min(radius, (height / 2) - epsilon, (width / 2) - epsilon);

ctx.moveTo(x + r, y);
ctx.lineTo(x + width - r, y);
ctx.arcTo(x + width, y, x + width, y + r, r);
ctx.lineTo(x + width, y + height - r);
ctx.arcTo(x + width, y + height, x + width - r, y + height, r);
ctx.lineTo(x + r, y + height);
ctx.arcTo(x, y + height, x, y + height - r, r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
} else {
ctx.rect(x, y, width, height);
}
Expand Down Expand Up @@ -89,7 +91,13 @@ var exports = module.exports = {
var topY = y - offset;
var sideSize = Math.SQRT2 * radius;
ctx.beginPath();
this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius / 2);

// NOTE(SB) the rounded rect implementation changed to use `arcTo`
// instead of `quadraticCurveTo` since it generates better results
// when rect is almost a circle. 0.425 (instead of 0.5) produces
// results visually closer to the previous impl.
this.roundedRect(ctx, leftX, topY, sideSize, sideSize, radius * 0.425);

ctx.closePath();
ctx.fill();
break;
Expand Down
1 change: 1 addition & 0 deletions test/jasmine.context.js
Expand Up @@ -75,6 +75,7 @@ Context.prototype._initMethods = function() {
var me = this;
var methods = {
arc: function() {},
arcTo: function() {},
beginPath: function() {},
bezierCurveTo: function() {},
clearRect: function() {},
Expand Down
2 changes: 1 addition & 1 deletion test/specs/element.point.tests.js
Expand Up @@ -222,7 +222,7 @@ describe('Point element tests', function() {
15 - offset,
Math.SQRT2 * 2,
Math.SQRT2 * 2,
2 / 2
2 * 0.425
);
expect(mockContext.getCalls()).toContain(
jasmine.objectContaining({
Expand Down
8 changes: 4 additions & 4 deletions test/specs/helpers.canvas.tests.js
Expand Up @@ -30,13 +30,13 @@ describe('Chart.helpers.canvas', function() {
expect(context.getCalls()).toEqual([
{name: 'moveTo', args: [15, 20]},
{name: 'lineTo', args: [35, 20]},
{name: 'quadraticCurveTo', args: [40, 20, 40, 25]},
{name: 'arcTo', args: [40, 20, 40, 25, 5]},
{name: 'lineTo', args: [40, 55]},
{name: 'quadraticCurveTo', args: [40, 60, 35, 60]},
{name: 'arcTo', args: [40, 60, 35, 60, 5]},
{name: 'lineTo', args: [15, 60]},
{name: 'quadraticCurveTo', args: [10, 60, 10, 55]},
{name: 'arcTo', args: [10, 60, 10, 55, 5]},
{name: 'lineTo', args: [10, 25]},
{name: 'quadraticCurveTo', args: [10, 20, 15, 20]}
{name: 'arcTo', args: [10, 20, 15, 20, 5]}
]);
});
it('should optimize path if radius is 0', function() {
Expand Down

0 comments on commit 88308c6

Please sign in to comment.