var Node = require('../Node'), Texture = require('../textures/Texture'), CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), GraphicsData = require('./GraphicsData'), math = require('../math'), CONST = require('../../const'), tempPoint = new math.Point(); /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and * rectangles to the display, and to color and fill them. * * @class * @extends Node */ class Graphics extends Node { /** * @constructor */ constructor() { super(); /** * The alpha value used when filling the Graphics object. * * @member {number} * @default 1 */ this.fillAlpha = 1; /** * The width (thickness) of any lines drawn. * * @member {number} * @default 0 */ this.lineWidth = 0; /** * The color of any lines drawn. * * @member {string} * @default 0 */ this.lineColor = 0; /** * Graphics data * * @member {GraphicsData[]} * @private */ this.graphicsData = []; /** * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to reset the tint. * * @member {number} * @default 0xFFFFFF */ this.tint = 0xFFFFFF; /** * The previous tint applied to the graphic shape. Used to compare to the current tint and check if theres change. * * @member {number} * @private * @default 0xFFFFFF */ this._prevTint = 0xFFFFFF; /** * The blend mode to be applied to the graphic shape. Apply a value of `BLEND_MODES.NORMAL` to reset the blend mode. * * @member {number} * @default BLEND_MODES.NORMAL; * @see BLEND_MODES */ this.blendMode = CONST.BLEND_MODES.NORMAL; /** * Current path * * @member {GraphicsData} * @private */ this.currentPath = null; /** * Array containing some WebGL-related properties used by the WebGL renderer. * * @member {object<number, object>} * @private */ // TODO - _webgl should use a prototype object, not a random undocumented object... this._webGL = {}; /** * Whether this shape is being used as a mask. * * @member {boolean} */ this.isMask = false; /** * The bounds' padding used for bounds calculation. * * @member {number} */ this.boundsPadding = 0; /** * A cache of the local bounds to prevent recalculation. * * @member {Rectangle} * @private */ this._localBounds = new math.Rectangle(0,0,1,1); /** * Used to detect if the graphics object has changed. If this is set to true then the graphics * object will be recalculated. * * @member {boolean} * @private */ this.dirty = true; /** * Used to detect if the WebGL graphics object has changed. If this is set to true then the * graphics object will be recalculated. * * @member {boolean} * @private */ this.glDirty = false; this.boundsDirty = true; /** * Used to detect if the cached sprite object needs to be updated. * * @member {boolean} * @private */ this.cachedSpriteDirty = false; } /** * Creates a new Graphics object with the same values as this one. * Note that the only the properties of the object are cloned, not its transform (position,scale,etc) * * @return {Graphics} A new graphics instance */ clone() { var clone = new Graphics(); clone.renderable = this.renderable; clone.fillAlpha = this.fillAlpha; clone.lineWidth = this.lineWidth; clone.lineColor = this.lineColor; clone.tint = this.tint; clone.blendMode = this.blendMode; clone.isMask = this.isMask; clone.boundsPadding = this.boundsPadding; clone.dirty = true; clone.glDirty = true; clone.cachedSpriteDirty = this.cachedSpriteDirty; // copy graphics data for (var i = 0; i < this.graphicsData.length; ++i) { clone.graphicsData.push(this.graphicsData[i].clone()); } clone.currentPath = clone.graphicsData[clone.graphicsData.length - 1]; clone.updateLocalBounds(); return clone; } /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. * * @param {number} lineWidth width of the line to draw, will update the objects stored style * @param {number} color color of the line to draw, will update the objects stored style * @param {number} alpha alpha of the line to draw, will update the objects stored style * @return {Graphics} Self for chaining */ lineStyle(lineWidth, color, alpha) { this.lineWidth = lineWidth || 0; this.lineColor = color || 0; this.lineAlpha = (alpha === undefined) ? 1 : alpha; if (this.currentPath) { if (this.currentPath.shape.points.length) { // halfway through a line? start a new one! var shape = new math.Polygon(this.currentPath.shape.points.slice(-2)); shape.closed = false; this.drawShape(shape); } else { // otherwise its empty so lets just set the line properties this.currentPath.lineWidth = this.lineWidth; this.currentPath.lineColor = this.lineColor; this.currentPath.lineAlpha = this.lineAlpha; } } return this; } /** * Moves the current drawing position to x, y. * * @param {number} x the X coordinate to move to * @param {number} y the Y coordinate to move to * @return {Graphics} Self for chaining */ moveTo(x, y) { var shape = new math.Polygon([x,y]); shape.closed = false; this.drawShape(shape); return this; } /** * Draws a line using the current line style from the current drawing position to (x, y); * The current drawing position is then set to (x, y). * * @param {number} x the X coordinate to draw to * @param {number} y the Y coordinate to draw to * @return {Graphics} Self for chaining */ lineTo(x, y) { this.currentPath.shape.points.push(x, y); this.dirty = true; return this; } /** * Calculate the points for a quadratic bezier curve and then draws it. * Based on: https://stackoverflow.com/questions/785097/how-do-i-implement-a-bezier-curve-in-c * * @param {number} cpX Control point x * @param {number} cpY Control point y * @param {number} toX Destination point x * @param {number} toY Destination point y * @return {Graphics} Self for chaining */ quadraticCurveTo(cpX, cpY, toX, toY) { if (this.currentPath) { if (this.currentPath.shape.points.length === 0) { this.currentPath.shape.points = [0, 0]; } } else { this.moveTo(0,0); } var xa, ya, n = 20, points = this.currentPath.shape.points; if (points.length === 0) { this.moveTo(0, 0); } var fromX = points[points.length - 2]; var fromY = points[points.length - 1]; var j = 0; for (var i = 1; i <= n; ++i) { j = i / n; xa = fromX + ((cpX - fromX) * j); ya = fromY + ((cpY - fromY) * j); points.push(xa + (((cpX + ((toX - cpX) * j)) - xa) * j), ya + (((cpY + ((toY - cpY) * j)) - ya) * j)); } this.dirty = this.boundsDirty = true; return this; } /** * Calculate the points for a bezier curve and then draws it. * * @param {number} cpX Control point x * @param {number} cpY Control point y * @param {number} cpX2 Second Control point x * @param {number} cpY2 Second Control point y * @param {number} toX Destination point x * @param {number} toY Destination point y * @return {Graphics} Self for chaining */ bezierCurveTo(cpX, cpY, cpX2, cpY2, toX, toY) { if (this.currentPath) { if (this.currentPath.shape.points.length === 0) { this.currentPath.shape.points = [0, 0]; } } else { this.moveTo(0,0); } var n = 20, dt, dt2, dt3, t2, t3, points = this.currentPath.shape.points; var fromX = points[points.length - 2]; var fromY = points[points.length - 1]; var j = 0; for (var i = 1; i <= n; ++i) { j = i / n; dt = (1 - j); dt2 = dt * dt; dt3 = dt2 * dt; t2 = j * j; t3 = t2 * j; points.push(dt3 * fromX + 3 * dt2 * j * cpX + 3 * dt * t2 * cpX2 + t3 * toX, dt3 * fromY + 3 * dt2 * j * cpY + 3 * dt * t2 * cpY2 + t3 * toY); } this.dirty = this.boundsDirty = true; return this; } /** * The arcTo() method creates an arc/curve between two tangents on the canvas. * * "borrowed" from https://code.google.com/p/fxcanvas/ - thanks google! * * @param {number} x1 The x-coordinate of the beginning of the arc * @param {number} y1 The y-coordinate of the beginning of the arc * @param {number} x2 The x-coordinate of the end of the arc * @param {number} y2 The y-coordinate of the end of the arc * @param {number} radius The radius of the arc * @return {Graphics} Self for chaining */ arcTo(x1, y1, x2, y2, radius) { if (this.currentPath) { if (this.currentPath.shape.points.length === 0) { this.currentPath.shape.points.push(x1, y1); } } else { this.moveTo(x1, y1); } var points = this.currentPath.shape.points, fromX = points[points.length - 2], fromY = points[points.length - 1], a1 = fromY - y1, b1 = fromX - x1, a2 = y2 - y1, b2 = x2 - x1, mm = Math.abs(a1 * b2 - b1 * a2); if (mm < 1.0e-8 || radius === 0) { if (points[points.length - 2] !== x1 || points[points.length - 1] !== y1) { points.push(x1, y1); } } else { var dd = a1 * a1 + b1 * b1, cc = a2 * a2 + b2 * b2, tt = a1 * a2 + b1 * b2, k1 = radius * Math.sqrt(dd) / mm, k2 = radius * Math.sqrt(cc) / mm, j1 = k1 * tt / dd, j2 = k2 * tt / cc, cx = k1 * b2 + k2 * b1, cy = k1 * a2 + k2 * a1, px = b1 * (k2 + j1), py = a1 * (k2 + j1), qx = b2 * (k1 + j2), qy = a2 * (k1 + j2), startAngle = Math.atan2(py - cy, px - cx), endAngle = Math.atan2(qy - cy, qx - cx); this.arc(cx + x1, cy + y1, radius, startAngle, endAngle, b1 * a2 > b2 * a1); } this.dirty = this.boundsDirty = true; return this; } /** * The arc method creates an arc/curve (used to create circles, or parts of circles). * * @param {number} cx The x-coordinate of the center of the circle * @param {number} cy The y-coordinate of the center of the circle * @param {number} radius The radius of the circle * @param {number} startAngle The starting angle, in radians (0 is at the 3 o'clock position of the arc's circle) * @param {number} endAngle The ending angle, in radians * @param {boolean} anticlockwise Optional. Specifies whether the drawing should be counterclockwise or clockwise. False is default, and indicates clockwise, while true indicates counter-clockwise. * @return {Graphics} Self for chaining */ arc(cx, cy, radius, startAngle, endAngle, anticlockwise) { anticlockwise = anticlockwise || false; if (startAngle === endAngle) { return this; } if (!anticlockwise && endAngle <= startAngle) { endAngle += Math.PI * 2; } else if (anticlockwise && startAngle <= endAngle) { startAngle += Math.PI * 2; } var sweep = anticlockwise ? (startAngle - endAngle) * -1 : (endAngle - startAngle); var segs = Math.ceil(Math.abs(sweep) / (Math.PI * 2)) * 40; if (sweep === 0) { return this; } var startX = cx + Math.cos(startAngle) * radius; var startY = cy + Math.sin(startAngle) * radius; if (this.currentPath) { this.currentPath.shape.points.push(startX, startY); } else { this.moveTo(startX, startY); } var points = this.currentPath.shape.points; var theta = sweep / (segs * 2); var theta2 = theta * 2; var cTheta = Math.cos(theta); var sTheta = Math.sin(theta); var segMinus = segs - 1; var remainder = (segMinus % 1) / segMinus; for (var i = 0; i <= segMinus; i++) { var real = i + remainder * i; var angle = ((theta) + startAngle + (theta2 * real)); var c = Math.cos(angle); var s = -Math.sin(angle); points.push(((cTheta * c) + (sTheta * s)) * radius + cx, ((cTheta * -s) + (sTheta * c)) * radius + cy); } this.dirty = this.boundsDirty = true; return this; } /** * Specifies a simple one-color fill that subsequent calls to other Graphics methods * (such as lineTo() or drawCircle()) use when drawing. * * @param {number} color the color of the fill * @param {number} alpha the alpha of the fill * @return {Graphics} Self for chaining */ beginFill(color, alpha) { this.filling = true; this.fillColor = color || 0; this.fillAlpha = (alpha === undefined) ? 1 : alpha; if (this.currentPath) { if (this.currentPath.shape.points.length <= 2) { this.currentPath.fill = this.filling; this.currentPath.fillColor = this.fillColor; this.currentPath.fillAlpha = this.fillAlpha; } } return this; } /** * Applies a fill to the lines and shapes that were added since the last call to the beginFill() method. * * @return {Graphics} Self for chaining */ endFill() { this.filling = false; this.fillColor = null; this.fillAlpha = 1; return this; } /** * Draw a rectangle * @param {number} x The X coord of the top-left of the rectangle * @param {number} y The Y coord of the top-left of the rectangle * @param {number} width The width of the rectangle * @param {number} height The height of the rectangle * @return {Graphics} Self for chaining */ drawRect(x, y, width, height) { this.drawShape(new math.Rectangle(x,y, width, height)); return this; } /** * * @param {number} x The X coord of the top-left of the rectangle * @param {number} y The Y coord of the top-left of the rectangle * @param {number} width The width of the rectangle * @param {number} height The height of the rectangle * @param {number} radius Radius of the rectangle corners * @return {Graphics} Self for chaining */ drawRoundedRect(x, y, width, height, radius) { this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); return this; } /** * Draws a circle. * * @param {number} x The X coordinate of the center of the circle * @param {number} y The Y coordinate of the center of the circle * @param {number} radius The radius of the circle * @return {Graphics} Self for chaining */ drawCircle(x, y, radius) { this.drawShape(new math.Circle(x,y, radius)); return this; } /** * Draws an ellipse. * * @param {number} x The X coordinate of the center of the ellipse * @param {number} y The Y coordinate of the center of the ellipse * @param {number} width The half width of the ellipse * @param {number} height The half height of the ellipse * @return {Graphics} Self for chaining */ drawEllipse(x, y, width, height) { this.drawShape(new math.Ellipse(x, y, width, height)); return this; } /** * Draws a polygon using the given path. * * @param {number[]|Point[]} path The path data used to construct the polygon. * @return {Graphics} Self for chaining */ drawPolygon(path) { // prevents an argument assignment deopt // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments var points = path; var closed = true; if (points instanceof math.Polygon) { closed = points.closed; points = points.points; } if (!Array.isArray(points)) { // prevents an argument leak deopt // see section 3.2: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments points = new Array(arguments.length); for (var i = 0; i < points.length; ++i) { points[i] = arguments[i]; } } var shape = new math.Polygon(points); shape.closed = closed; this.drawShape(shape); return this; } /** * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. * * @return {Graphics} Self for chaining */ clear() { this.lineWidth = 0; this.filling = false; this.dirty = true; this.clearDirty = true; this.graphicsData = []; return this; } /** * Useful function that returns a texture of the graphics object that can then be used to create sprites * This can be quite useful if your geometry is complicated and needs to be reused multiple times. * * @param {number} resolution The resolution of the texture being generated * @param {number} scaleMode Should be one of the scaleMode consts * @return {Texture} a texture of the graphics object */ generateTexture(resolution, scaleMode) { resolution = resolution || 1; var bounds = this.getLocalBounds(); var canvasBuffer = new CanvasBuffer(bounds.width * resolution, bounds.height * resolution); var texture = Texture.fromCanvas(canvasBuffer.canvas, scaleMode); texture.baseTexture.resolution = resolution; canvasBuffer.context.scale(resolution, resolution); canvasBuffer.context.translate(-bounds.x,-bounds.y); CanvasGraphics.renderGraphics(this, canvasBuffer.context); return texture; } /** * Renders the object using the WebGL renderer * * @param {WebGLRenderer} renderer Renderer to draw to * @private */ _renderWebGL(renderer) { if (this.glDirty) { this.dirty = true; this.glDirty = false; } renderer.setObjectRenderer(renderer.plugins.graphics); renderer.plugins.graphics.render(this); } /** * Renders the object using the Canvas renderer * * @param {CanvasRenderer} renderer Renderer to draw to * @private */ _renderCanvas(renderer) { if (this.isMask === true) { return; } // if the tint has changed, set the graphics object to dirty. if (this._prevTint !== this.tint) { this.dirty = true; } var context = renderer.context; var transform = this.worldTransform; var compositeOperation = renderer.blendModes[this.blendMode]; if (compositeOperation !== context.globalCompositeOperation) { context.globalCompositeOperation = compositeOperation; } var resolution = renderer.resolution; context.setTransform( transform.a * resolution, transform.b * resolution, transform.c * resolution, transform.d * resolution, transform.tx * resolution, transform.ty * resolution ); CanvasGraphics.renderGraphics(this, context); } /** * Retrieves the bounds of the graphic shape as a rectangle object * * @param {Matrix} [matrix] The world transform matrix to use, defaults to this * object's worldTransform. * @return {Rectangle} the rectangular bounding area */ getBounds(matrix) { if (!this._currentBounds) { // return an empty object if the item is a mask! if (!this.renderable) { return math.Rectangle.EMPTY; } if (this.boundsDirty) { this.updateLocalBounds(); this.glDirty = true; this.cachedSpriteDirty = true; this.boundsDirty = false; } var bounds = this._localBounds; var w0 = bounds.x; var w1 = bounds.width + bounds.x; var h0 = bounds.y; var h1 = bounds.height + bounds.y; var worldTransform = matrix || this.worldTransform; var a = worldTransform.a; var b = worldTransform.b; var c = worldTransform.c; var d = worldTransform.d; var tx = worldTransform.tx; var ty = worldTransform.ty; var x1 = a * w1 + c * h1 + tx; var y1 = d * h1 + b * w1 + ty; var x2 = a * w0 + c * h1 + tx; var y2 = d * h1 + b * w0 + ty; var x3 = a * w0 + c * h0 + tx; var y3 = d * h0 + b * w0 + ty; var x4 = a * w1 + c * h0 + tx; var y4 = d * h0 + b * w1 + ty; var maxX = x1; var maxY = y1; var minX = x1; var minY = y1; minX = x2 < minX ? x2 : minX; minX = x3 < minX ? x3 : minX; minX = x4 < minX ? x4 : minX; minY = y2 < minY ? y2 : minY; minY = y3 < minY ? y3 : minY; minY = y4 < minY ? y4 : minY; maxX = x2 > maxX ? x2 : maxX; maxX = x3 > maxX ? x3 : maxX; maxX = x4 > maxX ? x4 : maxX; maxY = y2 > maxY ? y2 : maxY; maxY = y3 > maxY ? y3 : maxY; maxY = y4 > maxY ? y4 : maxY; this._bounds.x = minX; this._bounds.width = maxX - minX; this._bounds.y = minY; this._bounds.height = maxY - minY; this._currentBounds = this._bounds; } return this._currentBounds; } /** * Tests if a point is inside this graphics object * * @param {Point} point the point to test * @return {boolean} the result of the test */ containsPoint(point) { this.worldTransform.applyInverse(point, tempPoint); var graphicsData = this.graphicsData; for (var i = 0; i < graphicsData.length; i++) { var data = graphicsData[i]; if (!data.fill) { continue; } // only deal with fills.. if (data.shape) { if (data.shape.contains(tempPoint.x, tempPoint.y)) { return true; } } } return false; } /** * Update the bounds of the object */ updateLocalBounds() { var minX = Infinity; var maxX = -Infinity; var minY = Infinity; var maxY = -Infinity; if (this.graphicsData.length) { var shape, points, x, y, w, h; for (var i = 0; i < this.graphicsData.length; i++) { var data = this.graphicsData[i]; var type = data.type; var lineWidth = data.lineWidth; shape = data.shape; if (type === CONST.SHAPES.RECT || type === CONST.SHAPES.RREC) { x = shape.x - lineWidth / 2; y = shape.y - lineWidth / 2; w = shape.width + lineWidth; h = shape.height + lineWidth; minX = x < minX ? x : minX; maxX = x + w > maxX ? x + w : maxX; minY = y < minY ? y : minY; maxY = y + h > maxY ? y + h : maxY; } else if (type === CONST.SHAPES.CIRC) { x = shape.x; y = shape.y; w = shape.radius + lineWidth / 2; h = shape.radius + lineWidth / 2; minX = x - w < minX ? x - w : minX; maxX = x + w > maxX ? x + w : maxX; minY = y - h < minY ? y - h : minY; maxY = y + h > maxY ? y + h : maxY; } else if (type === CONST.SHAPES.ELIP) { x = shape.x; y = shape.y; w = shape.width + lineWidth / 2; h = shape.height + lineWidth / 2; minX = x - w < minX ? x - w : minX; maxX = x + w > maxX ? x + w : maxX; minY = y - h < minY ? y - h : minY; maxY = y + h > maxY ? y + h : maxY; } else { // POLY points = shape.points; for (var j = 0; j < points.length; j += 2) { x = points[j]; y = points[j + 1]; minX = x - lineWidth < minX ? x - lineWidth : minX; maxX = x + lineWidth > maxX ? x + lineWidth : maxX; minY = y - lineWidth < minY ? y - lineWidth : minY; maxY = y + lineWidth > maxY ? y + lineWidth : maxY; } } } } else { minX = 0; maxX = 0; minY = 0; maxY = 0; } var padding = this.boundsPadding; this._localBounds.x = minX - padding; this._localBounds.width = (maxX - minX) + padding * 2; this._localBounds.y = minY - padding; this._localBounds.height = (maxY - minY) + padding * 2; } /** * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. * * @param {Circle|Rectangle|Ellipse|Line|Polygon} shape The shape object to draw. * @return {GraphicsData} The generated GraphicsData object. */ drawShape(shape) { if (this.currentPath) { // check current path! if (this.currentPath.shape.points.length <= 2) { this.graphicsData.pop(); } } this.currentPath = null; var data = new GraphicsData(this.lineWidth, this.lineColor, this.lineAlpha, this.fillColor, this.fillAlpha, this.filling, shape); this.graphicsData.push(data); if (data.type === CONST.SHAPES.POLY) { data.shape.closed = data.shape.closed || this.filling; this.currentPath = data; } this.dirty = this.boundsDirty = true; return data; } /** * Destroys the Graphics object. */ destroy() { Node.prototype.destroy.apply(this, arguments); // destroy each of the GraphicsData objects for (var i = 0; i < this.graphicsData.length; ++i) { this.graphicsData[i].destroy(); } // for each webgl data entry, destroy the WebGLGraphicsData for (var id in this._webgl) { for (var j = 0; j < this._webgl[id].data.length; ++j) { this._webgl[id].data[j].destroy(); } } this.graphicsData = null; this.currentPath = null; this._webgl = null; this._localBounds = null; } } module.exports = Graphics;