/* global G_vmlCanvasManager *//** * Provides specific methods to draw with 2D Canvas element. */Ext.define('Ext.draw.engine.Canvas', { extend: 'Ext.draw.Surface', isCanvas: true, requires: [ //<feature legacyBrowser> 'Ext.draw.engine.excanvas', //</feature> 'Ext.draw.Animator', 'Ext.draw.Color' ], config: { /** * @cfg {Boolean} highPrecision * True to have the Canvas use JavaScript Number instead of single precision floating point * for transforms. * * For example, when using data with big numbers to plot line series, the transformation * matrix of the canvas will have big elements. Due to the implementation of the SVGMatrix, * the elements are represented by 32-bits floats, which will work incorrectly. * To compensate for that, we enable the canvas context to perform all the transformations * in JavaScript. * * Do not use this if you are not encountering 32-bit floating point errors problem, * since this will result in a performance penalty. */ highPrecision: false }, statics: { contextOverrides: { /** * @ignore */ setGradientBBox: function(bbox) { this.bbox = bbox; }, /** * Fills the subpaths of the current default path or the given path with the current * fill style. * @ignore */ fill: function() { var fillStyle = this.fillStyle, fillGradient = this.fillGradient, fillOpacity = this.fillOpacity, alpha = this.globalAlpha, bbox = this.bbox; if (fillStyle !== Ext.util.Color.RGBA_NONE && fillOpacity !== 0) { if (fillGradient && bbox) { this.fillStyle = fillGradient.generateGradient(this, bbox); } if (fillOpacity !== 1) { this.globalAlpha = alpha * fillOpacity; } this.$fill(); if (fillOpacity !== 1) { this.globalAlpha = alpha; } if (fillGradient && bbox) { this.fillStyle = fillStyle; } } }, /** * Strokes the subpaths of the current default path or the given path with the current * stroke style. * @ignore */ stroke: function() { var strokeStyle = this.strokeStyle, strokeGradient = this.strokeGradient, strokeOpacity = this.strokeOpacity, alpha = this.globalAlpha, bbox = this.bbox; if (strokeStyle !== Ext.util.Color.RGBA_NONE && strokeOpacity !== 0) { if (strokeGradient && bbox) { this.strokeStyle = strokeGradient.generateGradient(this, bbox); } if (strokeOpacity !== 1) { this.globalAlpha = alpha * strokeOpacity; } this.$stroke(); if (strokeOpacity !== 1) { this.globalAlpha = alpha; } if (strokeGradient && bbox) { this.strokeStyle = strokeStyle; } } }, /** * @ignore */ fillStroke: function(attr, transformFillStroke) { var ctx = this, fillStyle = this.fillStyle, fillOpacity = this.fillOpacity, strokeStyle = this.strokeStyle, strokeOpacity = this.strokeOpacity, shadowColor = ctx.shadowColor, shadowBlur = ctx.shadowBlur, none = Ext.util.Color.RGBA_NONE; if (transformFillStroke === undefined) { transformFillStroke = attr.transformFillStroke; } if (!transformFillStroke) { attr.inverseMatrix.toContext(ctx); } if (fillStyle !== none && fillOpacity !== 0) { ctx.fill(); ctx.shadowColor = none; ctx.shadowBlur = 0; } if (strokeStyle !== none && strokeOpacity !== 0) { ctx.stroke(); } ctx.shadowColor = shadowColor; ctx.shadowBlur = shadowBlur; }, /** * 2D Canvas context in IE (up to IE10, inclusive) doesn't support * the setLineDash method and the lineDashOffset property. * @param dashList An even number of non-negative numbers specifying a dash list. */ setLineDash: function(dashList) { if (this.$setLineDash) { this.$setLineDash(dashList); } }, getLineDash: function() { if (this.$getLineDash) { return this.$getLineDash(); } }, /** * Adds points to the subpath such that the arc described by the circumference of the * ellipse described by the arguments, starting at the given start angle and ending at * the given end angle, going in the given direction (defaulting to clockwise), is added * to the path, connected to the previous point by a straight line. * @ignore */ ellipse: function(cx, cy, rx, ry, rotation, start, end, anticlockwise) { var cos = Math.cos(rotation), sin = Math.sin(rotation); this.transform(cos * rx, sin * rx, -sin * ry, cos * ry, cx, cy); this.arc(0, 0, 1, start, end, anticlockwise); this.transform( cos / rx, -sin / ry, sin / rx, cos / ry, -(cos * cx + sin * cy) / rx, (sin * cx - cos * cy) / ry); }, /** * Uses the given path commands to begin a new path on the canvas. * @ignore */ appendPath: function(path) { var me = this, i = 0, j = 0, commands = path.commands, params = path.params, ln = commands.length; me.beginPath(); for (; i < ln; i++) { switch (commands[i]) { case 'M': me.moveTo(params[j], params[j + 1]); j += 2; break; case 'L': me.lineTo(params[j], params[j + 1]); j += 2; break; case 'C': me.bezierCurveTo( params[j], params[j + 1], params[j + 2], params[j + 3], params[j + 4], params[j + 5] ); j += 6; break; case 'Z': me.closePath(); break; } } }, save: function() { var toSave = this.toSave, ln = toSave.length, obj = ln && {}, // Don't allocate memory if we don't have to. i = 0, key; for (; i < ln; i++) { key = toSave[i]; if (key in this) { obj[key] = this[key]; } } this.state.push(obj); this.$save(); }, restore: function() { var obj = this.state.pop(), key; if (obj) { for (key in obj) { this[key] = obj[key]; } } this.$restore(); } } }, splitThreshold: 3000, /** * @private * Properties to be saved/restored in the `save` and `restore` methods. */ toSave: ['fillGradient', 'strokeGradient'], /** * @property element * @inheritdoc */ element: { reference: 'element', children: [{ reference: 'bodyElement', style: { width: '100%', height: '100%', position: 'relative' } }] }, /** * @private * * Creates the canvas element. */ createCanvas: function() { var canvas, overrides, ctx, name; canvas = Ext.Element.create({ tag: 'canvas', cls: Ext.baseCSSPrefix + 'surface-canvas' }); // Emulate Canvas in IE8 with VML. if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas.dom); this.isVML = true; } overrides = Ext.draw.engine.Canvas.contextOverrides; ctx = canvas.dom.getContext('2d'); if (ctx.ellipse) { delete overrides.ellipse; } ctx.state = []; ctx.toSave = this.toSave; // Saving references to the native Canvas context methods that we'll be overriding. for (name in overrides) { ctx['$' + name] = ctx[name]; } Ext.apply(ctx, overrides); if (this.getHighPrecision()) { this.enablePrecisionCompensation(ctx); } else { this.disablePrecisionCompensation(ctx); } this.bodyElement.appendChild(canvas); this.canvases.push(canvas); this.contexts.push(ctx); }, updateHighPrecision: function(highPrecision) { var contexts = this.contexts, ln = contexts.length, i, context; for (i = 0; i < ln; i++) { context = contexts[i]; if (highPrecision) { this.enablePrecisionCompensation(context); } else { this.disablePrecisionCompensation(context); } } }, precisionNames: [ 'rect', 'fillRect', 'strokeRect', 'clearRect', 'moveTo', 'lineTo', 'arc', 'arcTo', 'save', 'restore', 'updatePrecisionCompensate', 'setTransform', 'transform', 'scale', 'translate', 'rotate', 'quadraticCurveTo', 'bezierCurveTo', 'createLinearGradient', 'createRadialGradient', 'fillText', 'strokeText', 'drawImage' ], /** * @private * Clears canvas of compensation for canvas' use of single precision floating point. * @param {CanvasRenderingContext2D} ctx The canvas context. */ disablePrecisionCompensation: function(ctx) { var regularOverrides = Ext.draw.engine.Canvas.contextOverrides, precisionOverrides = this.precisionNames, ln = precisionOverrides.length, i, name; for (i = 0; i < ln; i++) { name = precisionOverrides[i]; if (!(name in regularOverrides)) { delete ctx[name]; } } this.setDirty(true); }, /** * @private * Compensate for canvas' use of single precision floating point. * @param {CanvasRenderingContext2D} ctx The canvas context. */ enablePrecisionCompensation: function(ctx) { var surface = this, xx = 1, yy = 1, dx = 0, dy = 0, matrix = new Ext.draw.Matrix(), transStack = [], comp = {}, regularOverrides = Ext.draw.engine.Canvas.contextOverrides, originalCtx = ctx.constructor.prototype, /** * @cfg {Object} precisionOverrides * @ignore */ precisionOverrides = { toSave: surface.toSave, /** * Adds a new closed subpath to the path, representing the given rectangle. * @return {*} * @ignore */ rect: function(x, y, w, h) { return originalCtx.rect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy); }, /** * Paints the given rectangle onto the canvas, using the current fill style. * @ignore */ fillRect: function(x, y, w, h) { this.updatePrecisionCompensateRect(); originalCtx.fillRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy); this.updatePrecisionCompensate(); }, /** * Paints the box that outlines the given rectangle onto the canvas, using * the current stroke style. * @ignore */ strokeRect: function(x, y, w, h) { this.updatePrecisionCompensateRect(); originalCtx.strokeRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy); this.updatePrecisionCompensate(); }, /** * Clears all pixels on the canvas in the given rectangle to transparent black. * @ignore */ clearRect: function(x, y, w, h) { return originalCtx.clearRect.call(this, x * xx + dx, y * yy + dy, w * xx, h * yy); }, /** * Creates a new subpath with the given point. * @ignore */ moveTo: function(x, y) { return originalCtx.moveTo.call(this, x * xx + dx, y * yy + dy); }, /** * Adds the given point to the current subpath, connected to the previous one * by a straight line. * @ignore */ lineTo: function(x, y) { return originalCtx.lineTo.call(this, x * xx + dx, y * yy + dy); }, /** * Adds points to the subpath such that the arc described by the circumference * of the circle described by the arguments, starting at the given start angle * and ending at the given end angle, going in the given direction (defaulting * to clockwise), is added to the path, connected to the previous point * by a straight line. * @ignore */ arc: function(x, y, radius, startAngle, endAngle, anticlockwise) { this.updatePrecisionCompensateRect(); originalCtx.arc.call(this, x * xx + dx, y * xx + dy, radius * xx, startAngle, endAngle, anticlockwise); this.updatePrecisionCompensate(); }, /** * Adds an arc with the given control points and radius to the current subpath, * connected to the previous point by a straight line. If two radii are provided, * the first controls the width of the arc's ellipse, and the second controls * the height. If only one is provided, or if they are the same, the arc is from * a circle. * * In the case of an ellipse, the rotation argument controls the clockwise * inclination of the ellipse relative to the x-axis. * @ignore */ arcTo: function(x1, y1, x2, y2, radius) { this.updatePrecisionCompensateRect(); originalCtx.arcTo.call(this, x1 * xx + dx, y1 * yy + dy, x2 * xx + dx, y2 * yy + dy, radius * xx); this.updatePrecisionCompensate(); }, /** * Pushes the context state to the state stack. * @ignore */ save: function() { transStack.push(matrix); matrix = matrix.clone(); regularOverrides.save.call(this); originalCtx.save.call(this); }, /** * Pops the state stack and restores the state. * @ignore */ restore: function() { matrix = transStack.pop(); regularOverrides.restore.call(this); originalCtx.restore.call(this); this.updatePrecisionCompensate(); }, /** * @ignore */ updatePrecisionCompensate: function() { matrix.precisionCompensate(surface.devicePixelRatio, comp); xx = comp.xx; yy = comp.yy; dx = comp.dx; dy = comp.dy; originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0); }, /** * @ignore */ updatePrecisionCompensateRect: function() { matrix.precisionCompensateRect(surface.devicePixelRatio, comp); xx = comp.xx; yy = comp.yy; dx = comp.dx; dy = comp.dy; originalCtx.setTransform.call(this, surface.devicePixelRatio, comp.b, comp.c, comp.d, 0, 0); }, /** * Changes the transformation matrix to the matrix given by the arguments * as described below. * @ignore */ setTransform: function(x2x, x2y, y2x, y2y, newDx, newDy) { matrix.set(x2x, x2y, y2x, y2y, newDx, newDy); this.updatePrecisionCompensate(); }, /** * Changes the transformation matrix to apply the matrix given by the arguments * as described below. * @ignore */ transform: function(x2x, x2y, y2x, y2y, newDx, newDy) { matrix.append(x2x, x2y, y2x, y2y, newDx, newDy); this.updatePrecisionCompensate(); }, /** * Scales the transformation matrix. * @return {*} * @ignore */ scale: function(sx, sy) { this.transform(sx, 0, 0, sy, 0, 0); }, /** * Translates the transformation matrix. * @return {*} * @ignore */ translate: function(dx, dy) { this.transform(1, 0, 0, 1, dx, dy); }, /** * Rotates the transformation matrix. * @return {*} * @ignore */ rotate: function(radians) { var cos = Math.cos(radians), sin = Math.sin(radians); this.transform(cos, sin, -sin, cos, 0, 0); }, /** * Adds the given point to the current subpath, connected to the previous one by a * quadratic Bézier curve with the given control point. * @return {*} * @ignore */ quadraticCurveTo: function(cx, cy, x, y) { originalCtx.quadraticCurveTo.call(this, cx * xx + dx, cy * yy + dy, x * xx + dx, y * yy + dy); }, /** * Adds the given point to the current subpath, connected to the previous one * by a cubic Bézier curve with the given control points. * @return {*} * @ignore */ bezierCurveTo: function(c1x, c1y, c2x, c2y, x, y) { originalCtx.bezierCurveTo.call(this, c1x * xx + dx, c1y * yy + dy, c2x * xx + dx, c2y * yy + dy, x * xx + dx, y * yy + dy); }, /** * Returns an object that represents a linear gradient that paints along the line * given by the coordinates represented by the arguments. * @return {*} * @ignore */ createLinearGradient: function(x0, y0, x1, y1) { var grad; this.updatePrecisionCompensateRect(); grad = originalCtx.createLinearGradient.call(this, x0 * xx + dx, y0 * yy + dy, x1 * xx + dx, y1 * yy + dy); this.updatePrecisionCompensate(); return grad; }, /** * Returns a CanvasGradient object that represents a radial gradient that paints * along the cone given by the circles represented by the arguments. If either * of the radii are negative, throws an IndexSizeError exception. * @return {*} * @ignore */ createRadialGradient: function(x0, y0, r0, x1, y1, r1) { var grad; this.updatePrecisionCompensateRect(); grad = originalCtx.createLinearGradient.call(this, x0 * xx + dx, y0 * xx + dy, r0 * xx, x1 * xx + dx, y1 * xx + dy, r1 * xx); this.updatePrecisionCompensate(); return grad; }, /** * Fills the given text at the given position. If a maximum width is provided, * the text will be scaled to fit that width if necessary. * @ignore */ fillText: function(text, x, y, maxWidth) { originalCtx.setTransform.apply(this, matrix.elements); if (typeof maxWidth === 'undefined') { originalCtx.fillText.call(this, text, x, y); } else { originalCtx.fillText.call(this, text, x, y, maxWidth); } this.updatePrecisionCompensate(); }, /** * Strokes the given text at the given position. If a * maximum width is provided, the text will be scaled to * fit that width if necessary. * @ignore */ strokeText: function(text, x, y, maxWidth) { originalCtx.setTransform.apply(this, matrix.elements); if (typeof maxWidth === 'undefined') { originalCtx.strokeText.call(this, text, x, y); } else { originalCtx.strokeText.call(this, text, x, y, maxWidth); } this.updatePrecisionCompensate(); }, /** * Fills the subpaths of the current default path or the given path with the current * fill style. * @ignore */ fill: function() { var fillGradient = this.fillGradient, bbox = this.bbox; this.updatePrecisionCompensateRect(); if (fillGradient && bbox) { this.fillStyle = fillGradient.generateGradient(this, bbox); } originalCtx.fill.call(this); this.updatePrecisionCompensate(); }, /** * Strokes the subpaths of the current default path or the given path with the * current stroke style. * @ignore */ stroke: function() { var strokeGradient = this.strokeGradient, bbox = this.bbox; this.updatePrecisionCompensateRect(); if (strokeGradient && bbox) { this.strokeStyle = strokeGradient.generateGradient(this, bbox); } originalCtx.stroke.call(this); this.updatePrecisionCompensate(); }, /** * Draws the given image onto the canvas. If the first argument isn't an img, * canvas, or video element, throws a TypeMismatchError exception. If the image * has no image data, throws an InvalidStateError exception. If the one of the * source rectangle dimensions is zero, throws an IndexSizeError exception. * If the image isn't yet fully decoded, then nothing is drawn. * @return {*} * @ignore */ drawImage: function(img_elem, arg1, arg2, arg3, arg4, dst_x, dst_y, dw, dh) { switch (arguments.length) { case 3: return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy); case 5: return originalCtx.drawImage.call(this, img_elem, arg1 * xx + dx, arg2 * yy + dy, arg3 * xx, arg4 * yy); case 9: return originalCtx.drawImage.call(this, img_elem, arg1, arg2, arg3, arg4, dst_x * xx + dx, dst_y * yy * dy, dw * xx, dh * yy); } } }; Ext.apply(ctx, precisionOverrides); this.setDirty(true); }, /** * Normally, a surface will have a single canvas. * However, on certain platforms/browsers there's a limit to how big a canvas can be. * 'splitThreshold' is used to determine maximum width/height of a single canvas element. * When a surface is wider/taller than the splitThreshold, extra canvas element(s) * will be created and tiled inside the surface. */ updateRect: function(rect) { this.callParent([rect]); // eslint-disable-next-line vars-on-top var me = this, l = Math.floor(rect[0]), t = Math.floor(rect[1]), r = Math.ceil(rect[0] + rect[2]), b = Math.ceil(rect[1] + rect[3]), devicePixelRatio = me.devicePixelRatio, canvases = me.canvases, w = r - l, h = b - t, splitThreshold = Math.round(me.splitThreshold / devicePixelRatio), xSplits = me.xSplits = Math.ceil(w / splitThreshold), ySplits = me.ySplits = Math.ceil(h / splitThreshold), i, j, k, offsetX, offsetY, dom, width, height; for (j = 0, offsetY = 0; j < ySplits; j++, offsetY += splitThreshold) { for (i = 0, offsetX = 0; i < xSplits; i++, offsetX += splitThreshold) { k = j * xSplits + i; if (k >= canvases.length) { me.createCanvas(); } dom = canvases[k].dom; dom.style.left = offsetX + 'px'; dom.style.top = offsetY + 'px'; // The Canvas doesn't automatically support hi-DPI displays. // We have to actually create a larger canvas (more pixels) // while keeping its physical size the same. height = Math.min(splitThreshold, h - offsetY); if (height * devicePixelRatio !== dom.height) { dom.height = height * devicePixelRatio; dom.style.height = height + 'px'; } width = Math.min(splitThreshold, w - offsetX); if (width * devicePixelRatio !== dom.width) { dom.width = width * devicePixelRatio; dom.style.width = width + 'px'; } me.applyDefaults(me.contexts[k]); } } me.activeCanvases = k = xSplits * ySplits; while (canvases.length > k) { canvases.pop().destroy(); } me.clear(); }, /** * @method clearTransform * @inheritdoc */ clearTransform: function() { var me = this, xSplits = me.xSplits, ySplits = me.ySplits, contexts = me.contexts, splitThreshold = me.splitThreshold, devicePixelRatio = me.devicePixelRatio, i, j, k, ctx; for (i = 0; i < xSplits; i++) { for (j = 0; j < ySplits; j++) { k = j * xSplits + i; ctx = contexts[k]; ctx.translate( -splitThreshold * i, -splitThreshold * j ); ctx.scale( devicePixelRatio, devicePixelRatio ); me.matrix.toContext(ctx); } } }, /** * @method renderSprite * @inheritdoc */ renderSprite: function(sprite) { var me = this, rect = me.getRect(), surfaceMatrix = me.matrix, parent = sprite.getParent(), matrix = Ext.draw.Matrix.fly([1, 0, 0, 1, 0, 0]), splitThreshold = me.splitThreshold / me.devicePixelRatio, xSplits = me.xSplits, ySplits = me.ySplits, offsetX, offsetY, ctx, bbox, width, height, left = 0, right, top = 0, bottom, w = rect[2], h = rect[3], i, j, k; while (parent && parent.isSprite) { matrix.prependMatrix(parent.matrix || parent.attr && parent.attr.matrix); parent = parent.getParent(); } matrix.prependMatrix(surfaceMatrix); bbox = sprite.getBBox(); if (bbox) { bbox = matrix.transformBBox(bbox); } sprite.preRender(me); if (sprite.attr.hidden || sprite.attr.globalAlpha === 0) { sprite.setDirty(false); return; } // Render this sprite on all Canvas elements it spans, skipping the rest. for (j = 0, offsetY = 0; j < ySplits; j++, offsetY += splitThreshold) { for (i = 0, offsetX = 0; i < xSplits; i++, offsetX += splitThreshold) { k = j * xSplits + i; ctx = me.contexts[k]; width = Math.min(splitThreshold, w - offsetX); height = Math.min(splitThreshold, h - offsetY); left = offsetX; right = left + width; top = offsetY; bottom = top + height; if (bbox) { if (bbox.x > right || bbox.x + bbox.width < left || bbox.y > bottom || bbox.y + bbox.height < top) { continue; } } ctx.save(); sprite.useAttributes(ctx, rect); if (false === sprite.render(me, ctx, [left, top, width, height])) { return false; } ctx.restore(); } } sprite.setDirty(false); }, flatten: function(size, surfaces) { var targetCanvas = document.createElement('canvas'), className = Ext.getClassName(this), ratio = this.devicePixelRatio, ctx = targetCanvas.getContext('2d'), surface, canvas, rect, i, j, xy; targetCanvas.width = Math.ceil(size.width * ratio); targetCanvas.height = Math.ceil(size.height * ratio); for (i = 0; i < surfaces.length; i++) { surface = surfaces[i]; if (Ext.getClassName(surface) !== className) { continue; } rect = surface.getRect(); for (j = 0; j < surface.canvases.length; j++) { canvas = surface.canvases[j]; xy = canvas.getOffsetsTo(canvas.getParent()); ctx.drawImage(canvas.dom, (rect[0] + xy[0]) * ratio, (rect[1] + xy[1]) * ratio); } } return { data: targetCanvas.toDataURL(), type: 'png' }; }, applyDefaults: function(ctx) { var none = Ext.util.Color.RGBA_NONE; ctx.strokeStyle = none; ctx.fillStyle = none; ctx.textAlign = 'start'; ctx.textBaseline = 'alphabetic'; ctx.miterLimit = 1; }, /** * @method clear * @inheritdoc */ clear: function() { var me = this, activeCanvases = me.activeCanvases, i, canvas, ctx; for (i = 0; i < activeCanvases; i++) { canvas = me.canvases[i].dom; ctx = me.contexts[i]; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); } me.setDirty(true); }, /** * Destroys the Canvas element and prepares it for Garbage Collection. */ destroy: function() { var me = this, canvases = me.canvases, ln = canvases.length, i; for (i = 0; i < ln; i++) { me.contexts[i] = null; canvases[i].destroy(); canvases[i] = null; } me.contexts = me.canvases = null; me.callParent(); }, privates: { initElement: function() { var me = this; me.callParent(); me.canvases = []; me.contexts = []; me.activeCanvases = me.xSplits = me.ySplits = 0; } }}, function() { var me = this, proto = me.prototype, splitThreshold = 1e10; if (Ext.os.is.Android4 && Ext.browser.is.Chrome) { splitThreshold = 3000; } else if (Ext.is.iOS) { splitThreshold = 2200; } proto.splitThreshold = splitThreshold;});