/** * This singleton applies overrides to the '2d' context of the HTML5 Canvas element * to make it resolution independent. */Ext.define('Ext.d3.canvas.HiDPI', { singleton: true, relFontSizeRegEx: /(^[0-9])*(\d+(?:\.\d+)?)(px|%|em|pt)/, absFontSizeRegEx: /(xx-small)|(x-small)|(small)|(medium)|(large)|(x-large)|(xx-large)/, /** * Methods [M] and properties [P] that don't correspond to a number of pixels * (or do, but aren't meant to be resolution independent) and don't need to be overriden: * - [M] save * - [M] restore * - [M] scale * - [M] rotate * - [M] beginPath * - [M] closePath * - [M] clip * - [M] createImageData * - [M] getImageData * - [M] drawFocusIfNeeded * - [M] createPattern * * - [P] shadowBlur * - [P] shadowColor * - [P] textAlign * - [P] textBaseline * - [P] fillStyle * - [P] strokeStyle * - [P] lineCap * - [P] globalAlpha * - [P] globalCompositeOperation * * `fill` and `stroke` methods still have to be overriden, because we cannot * override context properties (that do correspond to a number of pixels). * But because context properties are not used until a fill or stroke operation * is performed, we can postpone scaling them, and do it when `fill` and `stroke` * methods are called. */ /** * @private */ fillStroke: function (ctx, ratio, method) { var font = ctx.font, fontChanged = font !== ctx.$oldFont, shadowOffsetX = ctx.shadowOffsetX, shadowOffsetY = ctx.shadowOffsetY, lineDashOffset = ctx.lineDashOffset, lineWidth = ctx.lineWidth, scaledFont; if (fontChanged) { // No support for shorthands like 'caption', 'icon', 'menu', etc. // Sample font shorthand: // italic small-caps 400 1.2em/110% "Times New Roman", serif scaledFont = font.replace(this.relFontSizeRegEx, function (match, p1, p2) { return parseFloat(p2) * ratio; }); scaledFont = scaledFont.replace(this.absFontSizeRegEx, function (match, p1, p2, p3, p4, p5, p6, p7) { if (p1) { return 9 * ratio + 'px'; // xx-small } if (p2) { return 10 * ratio + 'px'; // x-small } if (p3) { return 13 * ratio + 'px'; // small } if (p4) { return 16 * ratio + 'px'; // medium } if (p5) { return 18 * ratio + 'px'; // large } if (p6) { return 24 * ratio + 'px'; // x-large } if (p7) { return 32 * ratio + 'px'; // xx-large } }); ctx.font = scaledFont; ctx.$oldFont = font; ctx.$scaledFont = scaledFont; } else { ctx.font = ctx.$scaledFont; } ctx.shadowOffsetX = shadowOffsetX * ratio; ctx.shadowOffsetY = shadowOffsetY * ratio; ctx.lineDashOffset = lineDashOffset * ratio; ctx.lineWidth = lineWidth * ratio; if (arguments.length > 3) { ctx[method].apply(ctx, Array.prototype.slice.call(arguments, 3)); } else { ctx[method](); } ctx.font = font; ctx.shadowOffsetX = shadowOffsetX; ctx.shadowOffsetY = shadowOffsetY; ctx.lineDashOffset = lineDashOffset; ctx.lineWidth = lineWidth; }, /** * @private */ getOverrides: function () { var me = this, ratio = me.getDevicePixelRatio(); return this.overrides || (this.overrides = { fill: function () { me.fillStroke(this, ratio, '$fill'); }, stroke: function () { me.fillStroke(this, ratio, '$stroke'); }, moveTo: function (x, y) { this.$moveTo(x * ratio, y * ratio); }, lineTo: function (x, y) { this.$lineTo(x * ratio, y * ratio); }, quadraticCurveTo: function (cpx, cpy, x, y) { this.$quadraticCurveTo(cpx * ratio, cpy * ratio, x * ratio, y * ratio); }, bezierCurveTo: function (cp1x, cp1y, cp2x, cp2y, x, y) { this.$bezierCurveTo( cp1x * ratio, cp1y * ratio, cp2x * ratio, cp2y * ratio, x * ratio, y * ratio ); }, arcTo: function (x1, y1, x2, y2, radius) { this.$arcTo(x1 * ratio, y1 * ratio, x2 * ratio, y2 * ratio, radius * ratio); }, arc: function (x, y, radius, startAngle, endAngle, counterclockwise) { this.$arc( x * ratio, y * ratio, radius * ratio, startAngle, endAngle, counterclockwise ); }, rect: function (x, y, width, height) { this.$rect(x * ratio, y * ratio, width * ratio, height * ratio); }, clearRect: function (x, y, width, height) { this.$clearRect(x * ratio, y * ratio, width * ratio, height * ratio); }, fillRect: function (x, y, width, height) { me.fillStroke(this, ratio, '$fillRect', x * ratio, y * ratio, width * ratio, height * ratio); }, strokeRect: function (x, y, width, height) { me.fillStroke(this, ratio, '$strokeRect', x * ratio, y * ratio, width * ratio, height * ratio); }, translate: function (x, y) { this.$translate(x * ratio, y * ratio); }, transform: function (xx, yx, xy, yy, dx, dy) { this.$transform(xx, yx, xy, yy, dx * ratio, dy * ratio); }, setTransform: function (xx, yx, xy, yy, dx, dy) { this.$setTransform(xx, yx, xy, yy, dx * ratio, dy * ratio); }, isPointInPath: function (path, x, y, fillRule) { var n = arguments.length; if (n > 3) { return this.$isPointInPath(path, x * ratio, y * ratio, fillRule); } else if (n > 2) { return this.$isPointInPath(path * ratio, x * ratio, y); } else { return this.$isPointInPath(path * ratio, x * ratio); } }, isPointInStroke: function (path, x, y) { var n = arguments.length; if (n > 2) { return this.$isPointInStroke(path, x * ratio, y * ratio); } else { return this.$isPointInStroke(path * ratio, x * ratio); } }, drawImage: function (image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) { var n = arguments.length; if (n > 5) { this.$drawImage(image, sx, sy, sWidth, sHeight, dx * ratio, dy * ratio, dWidth * ratio, dHeight * ratio); } else if (n > 3) { // sx, sy, sWidth, sHeight are actually dx, dy, dWidth, dHeight in this case, // i.e. destination canvas coordinates and dimensions, and have to be scaled. this.$drawImage(image, sx * ratio, sy * ratio, sWidth * ratio, sHeight * ratio ); } else { // sx and sy are actually dx and dy in this case, // i.e. destination canvas coordinates and have to be scaled. this.$drawImage(image, sx * ratio, sy * ratio); } }, putImageData: function (imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) { this.$putImageData(imagedata, dx * ratio, dy * ratio, dirtyX, dirtyY, dirtyWidth, dirtyHeight); }, setLineDash: function (segments) { if (!(segments instanceof Array)) { segments = Array.prototype.slice.call(segments); } this.$setLineDash(segments.map(function (v) { return v * ratio; })); }, fillText: function (text, x, y, maxWidth) { this.$fillText(text, x * ratio, y * ratio, maxWidth * ratio); }, strokeText: function (text, x, y, maxWidth) { this.$strokeText(text, x * ratio, y * ratio, maxWidth * ratio); }, measureText: function (text) { var result = this.$measureText(text); result.width *= ratio; return result; }, createLinearGradient: function (x0, y0, x1, y1) { return this.$createLinearGradient( x0 * ratio, y0 * ratio, x1 * ratio, y1 * ratio ); }, createRadialGradient: function (x0, y0, r0, x1, y1, r1) { return this.$createRadialGradient( x0 * ratio, y0 * ratio, r0 * ratio, x1 * ratio, y1 * ratio, r1 * ratio ); } }); }, /** * Gets device pixel ratio of the `window` object or the given Canvas element. * If given a Canvas element without {@link #applyOverrides overrides}, * the method will return 1, regardless of the actual device pixel ratio. * @param {HTMLCanvasElement} [canvas] * @return {Number} */ getDevicePixelRatio: function (canvas) { if (canvas) { return canvas.$devicePixelRatio || 1; } // `devicePixelRatio` is only supported from IE11, // so we use `deviceXDPI` and `logicalXDPI` that are supported from IE6. return window.devicePixelRatio || window.screen && window.screen.deviceXDPI / window.screen.logicalXDPI || 1; }, /** * Enables resolution independed drawing for the given Canvas element, * if device pixel ratio is not 1. * @param {HTMLCanvasElement} canvas * @return {HTMLCanvasElement} The given canvas element. */ applyOverrides: function (canvas) { var ctx = canvas.getContext('2d'), ratio = this.getDevicePixelRatio(), overrides = this.getOverrides(), name; if (!(canvas.$devicePixelRatio || ratio === 1)) { // Save original ctx methods under a different name // by prefixing them with '$'. Original methods will // be called from overrides. for (name in overrides) { ctx['$' + name] = ctx[name]; } Ext.apply(ctx, overrides); // Take note of the pixel ratio, which should be used for this canvas // element from now on, e.g. when new size is given via 'setResize'. // This is because the overrides have already been applied for a certain // pixel ratio, and if we move the browser window to a screen with a // different pixel ratio, the overrides would have to change as well, // and the canvas would have to be rerendered by whoever's using it. // This is also complicated by the absense of any sort of event // that lets us know about a change in the device pixel ratio. canvas.$devicePixelRatio = ratio; } return canvas; }, /** * Sets the size of the Canvas element, taking device pixel ratio into account. * Note that resizing the Canvas will reset its context, e.g. * lineWidth will be set to 1, fillStyle to #000000, and so on. * @param {HTMLCanvasElement} canvas * @param {Number} width * @param {Number} height * @return {HTMLCanvasElement} The given canvas element. */ setSize: function (canvas, width, height) { var ratio = this.getDevicePixelRatio(canvas); canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio); canvas.style.width = Math.round(width) + 'px'; canvas.style.height = Math.round(height) + 'px'; return canvas; } });