]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Adjust the size of rectRounded/rectRot point to fit pointRadius (#5858)
authorAkihiko Kusanagi <nagi@nagi-p.com>
Wed, 28 Nov 2018 06:53:41 +0000 (14:53 +0800)
committerSimon Brunel <simonbrunel@users.noreply.github.com>
Wed, 28 Nov 2018 06:53:41 +0000 (07:53 +0100)
- Calculate the vertices of the shapes so that they are inscribed in the circle that has the radius of `pointRadius`
- Remove `translate()` and `rotate()` to fix the regression introduced by #5319
- Refactor `rectRounded` for better performance

12 files changed:
src/helpers/helpers.canvas.js
test/fixtures/controller.bubble/point-style.png
test/fixtures/controller.line/point-style.png
test/fixtures/controller.radar/point-style.png
test/fixtures/element.point/point-style-rect-rot.png
test/fixtures/element.point/point-style-rect-rounded.png
test/fixtures/element.point/rotation.js [new file with mode: 0644]
test/fixtures/element.point/rotation.png [new file with mode: 0644]
test/fixtures/helpers.canvas/rounded-rect.js [new file with mode: 0644]
test/fixtures/helpers.canvas/rounded-rect.png [new file with mode: 0644]
test/specs/element.point.tests.js
test/specs/helpers.canvas.tests.js

index 60fb6e1a2991230c000d6cb495a6b3c8827ed473..ea0c6f1cefee8a38b3e4f9e8f1eee9a0bc150179 100644 (file)
@@ -2,6 +2,13 @@
 
 var helpers = require('./helpers.core');
 
+var PI = Math.PI;
+var RAD_PER_DEG = PI / 180;
+var DOUBLE_PI = PI * 2;
+var HALF_PI = PI / 2;
+var QUARTER_PI = PI / 4;
+var TWO_THIRDS_PI = PI * 2 / 3;
+
 /**
  * @namespace Chart.helpers.canvas
  */
@@ -27,20 +34,28 @@ var exports = module.exports = {
         */
        roundedRect: function(ctx, x, y, width, height, radius) {
                if (radius) {
-                       // 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);
+                       var r = Math.min(radius, height / 2, width / 2);
+                       var left = x + r;
+                       var top = y + r;
+                       var right = x + width - r;
+                       var bottom = y + height - r;
+
+                       ctx.moveTo(x, top);
+                       if (left < right && top < bottom) {
+                               ctx.arc(left, top, r, -PI, -HALF_PI);
+                               ctx.arc(right, top, r, -HALF_PI, 0);
+                               ctx.arc(right, bottom, r, 0, HALF_PI);
+                               ctx.arc(left, bottom, r, HALF_PI, PI);
+                       } else if (left < right) {
+                               ctx.moveTo(left, y);
+                               ctx.arc(right, top, r, -HALF_PI, HALF_PI);
+                               ctx.arc(left, top, r, HALF_PI, PI + HALF_PI);
+                       } else if (top < bottom) {
+                               ctx.arc(left, top, r, -PI, 0);
+                               ctx.arc(left, bottom, r, 0, PI);
+                       } else {
+                               ctx.arc(left, top, r, -PI, PI);
+                       }
                        ctx.closePath();
                        ctx.moveTo(x, y);
                } else {
@@ -49,8 +64,8 @@ var exports = module.exports = {
        },
 
        drawPoint: function(ctx, style, radius, x, y, rotation) {
-               var type, edgeLength, xOffset, yOffset, height, size;
-               rotation = rotation || 0;
+               var type, xOffset, yOffset, size, cornerRadius;
+               var rad = (rotation || 0) * RAD_PER_DEG;
 
                if (style && typeof style === 'object') {
                        type = style.toString();
@@ -64,88 +79,97 @@ var exports = module.exports = {
                        return;
                }
 
-               ctx.save();
-               ctx.translate(x, y);
-               ctx.rotate(rotation * Math.PI / 180);
                ctx.beginPath();
 
                switch (style) {
                // Default includes circle
                default:
-                       ctx.arc(0, 0, radius, 0, Math.PI * 2);
+                       ctx.arc(x, y, radius, 0, DOUBLE_PI);
                        ctx.closePath();
                        break;
                case 'triangle':
-                       edgeLength = 3 * radius / Math.sqrt(3);
-                       height = edgeLength * Math.sqrt(3) / 2;
-                       ctx.moveTo(-edgeLength / 2, height / 3);
-                       ctx.lineTo(edgeLength / 2, height / 3);
-                       ctx.lineTo(0, -2 * height / 3);
+                       ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius);
+                       rad += TWO_THIRDS_PI;
+                       ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius);
+                       rad += TWO_THIRDS_PI;
+                       ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius);
                        ctx.closePath();
                        break;
-               case 'rect':
-                       size = 1 / Math.SQRT2 * radius;
-                       ctx.rect(-size, -size, 2 * size, 2 * size);
-                       break;
                case 'rectRounded':
-                       var offset = radius / Math.SQRT2;
-                       var leftX = -offset;
-                       var topY = -offset;
-                       var sideSize = Math.SQRT2 * radius;
-
-                       // 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);
+                       // NOTE: the rounded rect implementation changed to use `arc` instead of
+                       // `quadraticCurveTo` since it generates better results when rect is
+                       // almost a circle. 0.516 (instead of 0.5) produces results with visually
+                       // closer proportion to the previous impl and it is inscribed in the
+                       // circle with `radius`. For more details, see the following PRs:
+                       // https://github.com/chartjs/Chart.js/issues/5597
+                       // https://github.com/chartjs/Chart.js/issues/5858
+                       cornerRadius = radius * 0.516;
+                       size = radius - cornerRadius;
+                       xOffset = Math.cos(rad + QUARTER_PI) * size;
+                       yOffset = Math.sin(rad + QUARTER_PI) * size;
+                       ctx.arc(x - xOffset, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI);
+                       ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad);
+                       ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI);
+                       ctx.arc(x - yOffset, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI);
+                       ctx.closePath();
                        break;
+               case 'rect':
+                       if (!rotation) {
+                               size = Math.SQRT1_2 * radius;
+                               ctx.rect(x - size, y - size, 2 * size, 2 * size);
+                               break;
+                       }
+                       rad += QUARTER_PI;
+                       /* falls through */
                case 'rectRot':
-                       size = 1 / Math.SQRT2 * radius;
-                       ctx.moveTo(-size, 0);
-                       ctx.lineTo(0, size);
-                       ctx.lineTo(size, 0);
-                       ctx.lineTo(0, -size);
+                       xOffset = Math.cos(rad) * radius;
+                       yOffset = Math.sin(rad) * radius;
+                       ctx.moveTo(x - xOffset, y - yOffset);
+                       ctx.lineTo(x + yOffset, y - xOffset);
+                       ctx.lineTo(x + xOffset, y + yOffset);
+                       ctx.lineTo(x - yOffset, y + xOffset);
                        ctx.closePath();
                        break;
-               case 'cross':
-                       ctx.moveTo(0, radius);
-                       ctx.lineTo(0, -radius);
-                       ctx.moveTo(-radius, 0);
-                       ctx.lineTo(radius, 0);
-                       break;
                case 'crossRot':
-                       xOffset = Math.cos(Math.PI / 4) * radius;
-                       yOffset = Math.sin(Math.PI / 4) * radius;
-                       ctx.moveTo(-xOffset, -yOffset);
-                       ctx.lineTo(xOffset, yOffset);
-                       ctx.moveTo(-xOffset, yOffset);
-                       ctx.lineTo(xOffset, -yOffset);
+                       rad += QUARTER_PI;
+                       /* falls through */
+               case 'cross':
+                       xOffset = Math.cos(rad) * radius;
+                       yOffset = Math.sin(rad) * radius;
+                       ctx.moveTo(x - xOffset, y - yOffset);
+                       ctx.lineTo(x + xOffset, y + yOffset);
+                       ctx.moveTo(x + yOffset, y - xOffset);
+                       ctx.lineTo(x - yOffset, y + xOffset);
                        break;
                case 'star':
-                       ctx.moveTo(0, radius);
-                       ctx.lineTo(0, -radius);
-                       ctx.moveTo(-radius, 0);
-                       ctx.lineTo(radius, 0);
-                       xOffset = Math.cos(Math.PI / 4) * radius;
-                       yOffset = Math.sin(Math.PI / 4) * radius;
-                       ctx.moveTo(-xOffset, -yOffset);
-                       ctx.lineTo(xOffset, yOffset);
-                       ctx.moveTo(-xOffset, yOffset);
-                       ctx.lineTo(xOffset, -yOffset);
+                       xOffset = Math.cos(rad) * radius;
+                       yOffset = Math.sin(rad) * radius;
+                       ctx.moveTo(x - xOffset, y - yOffset);
+                       ctx.lineTo(x + xOffset, y + yOffset);
+                       ctx.moveTo(x + yOffset, y - xOffset);
+                       ctx.lineTo(x - yOffset, y + xOffset);
+                       rad += QUARTER_PI;
+                       xOffset = Math.cos(rad) * radius;
+                       yOffset = Math.sin(rad) * radius;
+                       ctx.moveTo(x - xOffset, y - yOffset);
+                       ctx.lineTo(x + xOffset, y + yOffset);
+                       ctx.moveTo(x + yOffset, y - xOffset);
+                       ctx.lineTo(x - yOffset, y + xOffset);
                        break;
                case 'line':
-                       ctx.moveTo(-radius, 0);
-                       ctx.lineTo(radius, 0);
+                       xOffset = Math.cos(rad) * radius;
+                       yOffset = Math.sin(rad) * radius;
+                       ctx.moveTo(x - xOffset, y - yOffset);
+                       ctx.lineTo(x + xOffset, y + yOffset);
                        break;
                case 'dash':
-                       ctx.moveTo(0, 0);
-                       ctx.lineTo(radius, 0);
+                       ctx.moveTo(x, y);
+                       ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius);
                        break;
                }
 
                ctx.fill();
                ctx.stroke();
-               ctx.restore();
        },
 
        clipArea: function(ctx, area) {
index f1d3b8168c07d1e70eda4c04dc50325d25c0b39c..d949141d81d4f46dbb844b9a33e63d0b2f81a3fa 100644 (file)
Binary files a/test/fixtures/controller.bubble/point-style.png and b/test/fixtures/controller.bubble/point-style.png differ
index f177fbfe15eea654d31a2e4e94bf21dc0644b431..d8b6ed6b47526a4e54662fda7781dadd67d9e4e4 100644 (file)
Binary files a/test/fixtures/controller.line/point-style.png and b/test/fixtures/controller.line/point-style.png differ
index 562cb620b054de95898cba7bbc3908b479bfe2da..3f73ff96f91a02142de3504a59531b8f954d59d2 100644 (file)
Binary files a/test/fixtures/controller.radar/point-style.png and b/test/fixtures/controller.radar/point-style.png differ
index 09c0adac3d3e32580845000e7188e63fb60b7c95..a7c128855892682594576c778eeafaf043fd7f36 100644 (file)
Binary files a/test/fixtures/element.point/point-style-rect-rot.png and b/test/fixtures/element.point/point-style-rect-rot.png differ
index a58e9e62361a96fb187cc342c58d89c954faaaa4..8b58b44303ac578744a267231301427f461475b8 100644 (file)
Binary files a/test/fixtures/element.point/point-style-rect-rounded.png and b/test/fixtures/element.point/point-style-rect-rounded.png differ
diff --git a/test/fixtures/element.point/rotation.js b/test/fixtures/element.point/rotation.js
new file mode 100644 (file)
index 0000000..b713f5d
--- /dev/null
@@ -0,0 +1,56 @@
+var gradient;
+
+var datasets = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle'].map(function(style, y) {
+       return {
+               pointStyle: style,
+               data: Array.apply(null, Array(17)).map(function(v, x) {
+                       return {x: x, y: 10 - y};
+               })
+       };
+});
+
+var angles = Array.apply(null, Array(17)).map(function(v, i) {
+       return -180 + i * 22.5;
+});
+
+module.exports = {
+       config: {
+               type: 'bubble',
+               data: {
+                       datasets: datasets
+               },
+               options: {
+                       responsive: false,
+                       legend: false,
+                       title: false,
+                       elements: {
+                               point: {
+                                       rotation: angles,
+                                       radius: 10,
+                                       backgroundColor: function(context) {
+                                               if (!gradient) {
+                                                       gradient = context.chart.ctx.createLinearGradient(0, 0, 512, 256);
+                                                       gradient.addColorStop(0, '#ff0000');
+                                                       gradient.addColorStop(1, '#0000ff');
+                                               }
+                                               return gradient;
+                                       },
+                                       borderColor: '#cccccc'
+                               }
+                       },
+                       layout: {
+                               padding: 20
+                       },
+                       scales: {
+                               xAxes: [{display: false}],
+                               yAxes: [{display: false}]
+                       }
+               }
+       },
+       options: {
+               canvas: {
+                       height: 256,
+                       width: 512
+               }
+       }
+};
diff --git a/test/fixtures/element.point/rotation.png b/test/fixtures/element.point/rotation.png
new file mode 100644 (file)
index 0000000..579c712
Binary files /dev/null and b/test/fixtures/element.point/rotation.png differ
diff --git a/test/fixtures/helpers.canvas/rounded-rect.js b/test/fixtures/helpers.canvas/rounded-rect.js
new file mode 100644 (file)
index 0000000..cbdedac
--- /dev/null
@@ -0,0 +1,39 @@
+var roundedRect = Chart.helpers.canvas.roundedRect;
+
+module.exports = {
+       config: {
+               type: 'line',
+               plugins: [{
+                       afterDraw: function(chart) {
+                               var ctx = chart.ctx;
+                               ctx.strokeStyle = '#0000ff';
+                               ctx.lineWidth = 4;
+                               ctx.fillStyle = '#00ff00';
+                               ctx.beginPath();
+                               roundedRect(ctx, 10, 10, 50, 50, 25);
+                               roundedRect(ctx, 70, 10, 100, 50, 25);
+                               roundedRect(ctx, 10, 70, 50, 100, 25);
+                               roundedRect(ctx, 70, 70, 100, 100, 25);
+                               roundedRect(ctx, 180, 10, 50, 50, 100);
+                               roundedRect(ctx, 240, 10, 100, 50, 100);
+                               roundedRect(ctx, 180, 70, 50, 100, 100);
+                               roundedRect(ctx, 240, 70, 100, 100, 100);
+                               roundedRect(ctx, 350, 10, 50, 50, 0);
+                               ctx.fill();
+                               ctx.stroke();
+                       }
+               }],
+               options: {
+                       scales: {
+                               xAxes: [{display: false}],
+                               yAxes: [{display: false}]
+                       }
+               }
+       },
+       options: {
+               canvas: {
+                       height: 256,
+                       width: 512
+               }
+       }
+};
diff --git a/test/fixtures/helpers.canvas/rounded-rect.png b/test/fixtures/helpers.canvas/rounded-rect.png
new file mode 100644 (file)
index 0000000..8973c9d
Binary files /dev/null and b/test/fixtures/helpers.canvas/rounded-rect.png differ
index 0f3a03e319d0fd7abf33213389df7a82056f0b1f..887250acd8de4b0392a5cc763c2b3043496ea230 100644 (file)
@@ -124,21 +124,12 @@ describe('Chart.elements.Point', function() {
                }, {
                        name: 'setFillStyle',
                        args: ['rgba(0,0,0,0.1)']
-               }, {
-                       name: 'save',
-                       args: []
-               }, {
-                       name: 'translate',
-                       args: [10, 15]
-               }, {
-                       name: 'rotate',
-                       args: [0]
                }, {
                        name: 'beginPath',
                        args: []
                }, {
                        name: 'arc',
-                       args: [0, 0, 2, 0, 2 * Math.PI]
+                       args: [10, 15, 2, 0, 2 * Math.PI]
                }, {
                        name: 'closePath',
                        args: [],
@@ -148,9 +139,6 @@ describe('Chart.elements.Point', function() {
                }, {
                        name: 'stroke',
                        args: []
-               }, {
-                       name: 'restore',
-                       args: []
                }]);
        });
 
index 1a342c1cb3bf741a8daad806b4673c984562e98a..ee42a414e6f2c4ddda1bbfd133166edc4b46daa7 100644 (file)
@@ -1,6 +1,8 @@
 'use strict';
 
 describe('Chart.helpers.canvas', function() {
+       describe('auto', jasmine.fixture.specs('helpers.canvas'));
+
        var helpers = Chart.helpers;
 
        describe('clear', function() {
@@ -28,15 +30,50 @@ describe('Chart.helpers.canvas', function() {
                        helpers.canvas.roundedRect(context, 10, 20, 30, 40, 5);
 
                        expect(context.getCalls()).toEqual([
-                               {name: 'moveTo', args: [15, 20]},
-                               {name: 'lineTo', args: [35, 20]},
-                               {name: 'arcTo', args: [40, 20, 40, 25, 5]},
-                               {name: 'lineTo', args: [40, 55]},
-                               {name: 'arcTo', args: [40, 60, 35, 60, 5]},
-                               {name: 'lineTo', args: [15, 60]},
-                               {name: 'arcTo', args: [10, 60, 10, 55, 5]},
-                               {name: 'lineTo', args: [10, 25]},
-                               {name: 'arcTo', args: [10, 20, 15, 20, 5]},
+                               {name: 'moveTo', args: [10, 25]},
+                               {name: 'arc', args: [15, 25, 5, -Math.PI, -Math.PI / 2]},
+                               {name: 'arc', args: [35, 25, 5, -Math.PI / 2, 0]},
+                               {name: 'arc', args: [35, 55, 5, 0, Math.PI / 2]},
+                               {name: 'arc', args: [15, 55, 5, Math.PI / 2, Math.PI]},
+                               {name: 'closePath', args: []},
+                               {name: 'moveTo', args: [10, 20]}
+                       ]);
+               });
+               it('should optimize path if radius is exactly half of height', function() {
+                       var context = window.createMockContext();
+
+                       helpers.canvas.roundedRect(context, 10, 20, 40, 30, 15);
+
+                       expect(context.getCalls()).toEqual([
+                               {name: 'moveTo', args: [10, 35]},
+                               {name: 'moveTo', args: [25, 20]},
+                               {name: 'arc', args: [35, 35, 15, -Math.PI / 2, Math.PI / 2]},
+                               {name: 'arc', args: [25, 35, 15, Math.PI / 2, Math.PI * 3 / 2]},
+                               {name: 'closePath', args: []},
+                               {name: 'moveTo', args: [10, 20]}
+                       ]);
+               });
+               it('should optimize path if radius is exactly half of width', function() {
+                       var context = window.createMockContext();
+
+                       helpers.canvas.roundedRect(context, 10, 20, 30, 40, 15);
+
+                       expect(context.getCalls()).toEqual([
+                               {name: 'moveTo', args: [10, 35]},
+                               {name: 'arc', args: [25, 35, 15, -Math.PI, 0]},
+                               {name: 'arc', args: [25, 45, 15, 0, Math.PI]},
+                               {name: 'closePath', args: []},
+                               {name: 'moveTo', args: [10, 20]}
+                       ]);
+               });
+               it('should optimize path if radius is exactly half of width and height', function() {
+                       var context = window.createMockContext();
+
+                       helpers.canvas.roundedRect(context, 10, 20, 30, 30, 15);
+
+                       expect(context.getCalls()).toEqual([
+                               {name: 'moveTo', args: [10, 35]},
+                               {name: 'arc', args: [25, 35, 15, -Math.PI, Math.PI]},
                                {name: 'closePath', args: []},
                                {name: 'moveTo', args: [10, 20]}
                        ]);