UNPKG

@azerion/phaser

Version:

A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers.

1,722 lines (1,404 loc) 103 kB
/** * @author Mat Groves http://matgroves.com/ @Doormat23 */ /** * The Graphics class contains methods used to draw primitive shapes such as lines, circles and rectangles to the display, and color and fill them. * * @class Graphics * @extends DisplayObjectContainer * @constructor */ PIXI.Graphics = function() { PIXI.DisplayObjectContainer.call(this); this.renderable = true; /** * The alpha value used when filling the Graphics object. * * @property fillAlpha * @type Number */ this.fillAlpha = 1; /** * The width (thickness) of any lines drawn. * * @property lineWidth * @type Number */ this.lineWidth = 0; /** * The color of any lines drawn. * * @property lineColor * @type String * @default 0 */ this.lineColor = 0; /** * Graphics data * * @property graphicsData * @type Array * @private */ this.graphicsData = []; /** * The tint applied to the graphic shape. This is a hex value. Apply a value of 0xFFFFFF to reset the tint. * * @property tint * @type Number * @default 0xFFFFFF */ this.tint = 0xFFFFFF; /** * The blend mode to be applied to the graphic shape. Apply a value of PIXI.blendModes.NORMAL to reset the blend mode. * * @property blendMode * @type Number * @default PIXI.blendModes.NORMAL; */ this.blendMode = PIXI.blendModes.NORMAL; /** * Current path * * @property currentPath * @type Object * @private */ this.currentPath = null; /** * Array containing some WebGL-related properties used by the WebGL renderer. * * @property _webGL * @type Array * @private */ this._webGL = []; /** * Whether this shape is being used as a mask. * * @property isMask * @type Boolean */ this.isMask = false; /** * The bounds' padding used for bounds calculation. * * @property boundsPadding * @type Number */ this.boundsPadding = 0; this._localBounds = new PIXI.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. * * @property dirty * @type Boolean * @private */ this.dirty = true; /** * Used to detect if the bounds have been invalidated, by this Graphics being cleared or drawn to. * If this is set to true then the updateLocalBounds is called once in the postUpdate method. * * @property _boundsDirty * @type Boolean * @private */ this._boundsDirty = false; /** * Used to detect if the webgl graphics object has changed. If this is set to true then the graphics object will be recalculated. * * @property webGLDirty * @type Boolean * @private */ this.webGLDirty = false; /** * Used to detect if the cached sprite object needs to be updated. * * @property cachedSpriteDirty * @type Boolean * @private */ this.cachedSpriteDirty = false; }; // constructor PIXI.Graphics.prototype = Object.create( PIXI.DisplayObjectContainer.prototype ); PIXI.Graphics.prototype.constructor = PIXI.Graphics; /** * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. * * @method lineStyle * @param lineWidth {Number} width of the line to draw, will update the objects stored style * @param color {Number} color of the line to draw, will update the objects stored style * @param alpha {Number} alpha of the line to draw, will update the objects stored style * @return {Graphics} */ PIXI.Graphics.prototype.lineStyle = function(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! this.drawShape(new PIXI.Polygon(this.currentPath.shape.points.slice(-2))); } 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. * * @method moveTo * @param x {Number} the X coordinate to move to * @param y {Number} the Y coordinate to move to * @return {Graphics} */ PIXI.Graphics.prototype.moveTo = function(x, y) { this.drawShape(new PIXI.Polygon([x, y])); 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). * * @method lineTo * @param x {Number} the X coordinate to draw to * @param y {Number} the Y coordinate to draw to * @return {Graphics} */ PIXI.Graphics.prototype.lineTo = function(x, y) { if (!this.currentPath) { this.moveTo(0, 0); } this.currentPath.shape.points.push(x, y); this.dirty = true; this._boundsDirty = 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 * * @method quadraticCurveTo * @param cpX {Number} Control point x * @param cpY {Number} Control point y * @param toX {Number} Destination point x * @param toY {Number} Destination point y * @return {Graphics} */ PIXI.Graphics.prototype.quadraticCurveTo = function(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 = true; this._boundsDirty = true; return this; }; /** * Calculate the points for a bezier curve and then draws it. * * @method bezierCurveTo * @param cpX {Number} Control point x * @param cpY {Number} Control point y * @param cpX2 {Number} Second Control point x * @param cpY2 {Number} Second Control point y * @param toX {Number} Destination point x * @param toY {Number} Destination point y * @return {Graphics} */ PIXI.Graphics.prototype.bezierCurveTo = function(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 = true; 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! * * @method arcTo * @param x1 {Number} The x-coordinate of the beginning of the arc * @param y1 {Number} The y-coordinate of the beginning of the arc * @param x2 {Number} The x-coordinate of the end of the arc * @param y2 {Number} The y-coordinate of the end of the arc * @param radius {Number} The radius of the arc * @return {Graphics} */ PIXI.Graphics.prototype.arcTo = function(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 = true; this._boundsDirty = true; return this; }; /** * The arc method creates an arc/curve (used to create circles, or parts of circles). * * @method arc * @param cx {Number} The x-coordinate of the center of the circle * @param cy {Number} The y-coordinate of the center of the circle * @param radius {Number} The radius of the circle * @param startAngle {Number} The starting angle, in radians (0 is at the 3 o'clock position of the arc's circle) * @param endAngle {Number} The ending angle, in radians * @param anticlockwise {Boolean} Optional. Specifies whether the drawing should be counterclockwise or clockwise. False is default, and indicates clockwise, while true indicates counter-clockwise. * @param segments {Number} Optional. The number of segments to use when calculating the arc. The default is 40. If you need more fidelity use a higher number. * @return {Graphics} */ PIXI.Graphics.prototype.arc = function(cx, cy, radius, startAngle, endAngle, anticlockwise, segments) { // If we do this we can never draw a full circle if (startAngle === endAngle) { return this; } if (anticlockwise === undefined) { anticlockwise = false; } if (segments === undefined) { segments = 40; } 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)) * segments; // Sweep check - moved here because we don't want to do the moveTo below if the arc fails if (sweep === 0) { return this; } var startX = cx + Math.cos(startAngle) * radius; var startY = cy + Math.sin(startAngle) * radius; if (anticlockwise && this.filling) { this.moveTo(cx, cy); } else { this.moveTo(startX, startY); } // currentPath will always exist after calling a moveTo 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 = true; 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. * * @method beginFill * @param color {Number} the color of the fill * @param alpha {Number} the alpha of the fill * @return {Graphics} */ PIXI.Graphics.prototype.beginFill = function(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. * * @method endFill * @return {Graphics} */ PIXI.Graphics.prototype.endFill = function() { this.filling = false; this.fillColor = null; this.fillAlpha = 1; return this; }; /** * @method drawRect * * @param x {Number} The X coord of the top-left of the rectangle * @param y {Number} The Y coord of the top-left of the rectangle * @param width {Number} The width of the rectangle * @param height {Number} The height of the rectangle * @return {Graphics} */ PIXI.Graphics.prototype.drawRect = function(x, y, width, height) { this.drawShape(new PIXI.Rectangle(x, y, width, height)); return this; }; /** * @method drawRoundedRect * @param x {Number} The X coord of the top-left of the rectangle * @param y {Number} The Y coord of the top-left of the rectangle * @param width {Number} The width of the rectangle * @param height {Number} The height of the rectangle * @param radius {Number} Radius of the rectangle corners. In WebGL this must be a value between 0 and 9. */ PIXI.Graphics.prototype.drawRoundedRect = function(x, y, width, height, radius) { this.drawShape(new PIXI.RoundedRectangle(x, y, width, height, radius)); return this; }; /** * Draws a circle. * * @method drawCircle * @param x {Number} The X coordinate of the center of the circle * @param y {Number} The Y coordinate of the center of the circle * @param diameter {Number} The diameter of the circle * @return {Graphics} */ PIXI.Graphics.prototype.drawCircle = function(x, y, diameter) { this.drawShape(new PIXI.Circle(x, y, diameter)); return this; }; /** * Draws an ellipse. * * @method drawEllipse * @param x {Number} The X coordinate of the center of the ellipse * @param y {Number} The Y coordinate of the center of the ellipse * @param width {Number} The half width of the ellipse * @param height {Number} The half height of the ellipse * @return {Graphics} */ PIXI.Graphics.prototype.drawEllipse = function(x, y, width, height) { this.drawShape(new PIXI.Ellipse(x, y, width, height)); return this; }; /** * Draws a polygon using the given path. * * @method drawPolygon * @param path {Array|Phaser.Polygon} The path data used to construct the polygon. Can either be an array of points or a Phaser.Polygon object. * @return {Graphics} */ PIXI.Graphics.prototype.drawPolygon = function(path) { if (path instanceof Phaser.Polygon || path instanceof PIXI.Polygon) { path = path.points; } // prevents an argument assignment deopt // see section 3.1: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments var points = path; 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]; } } this.drawShape(new Phaser.Polygon(points)); return this; }; /** * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. * * @method clear * @return {Graphics} */ PIXI.Graphics.prototype.clear = function() { this.lineWidth = 0; this.filling = false; this.dirty = true; this._boundsDirty = true; this.clearDirty = true; this.graphicsData = []; this.updateLocalBounds(); 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. * * @method generateTexture * @param [resolution=1] {Number} The resolution of the texture being generated * @param [scaleMode=0] {Number} Should be one of the PIXI.scaleMode consts * @param [padding=0] {Number} Add optional extra padding to the generated texture (default 0) * @return {Texture} a texture of the graphics object */ PIXI.Graphics.prototype.generateTexture = function(resolution, scaleMode, padding) { if (resolution === undefined) { resolution = 1; } if (scaleMode === undefined) { scaleMode = PIXI.scaleModes.DEFAULT; } if (padding === undefined) { padding = 0; } var bounds = this.getBounds(); bounds.width += padding; bounds.height += padding; var canvasBuffer = new PIXI.CanvasBuffer(bounds.width * resolution, bounds.height * resolution); var texture = PIXI.Texture.fromCanvas(canvasBuffer.canvas, scaleMode); texture.baseTexture.resolution = resolution; canvasBuffer.context.scale(resolution, resolution); canvasBuffer.context.translate(-bounds.x, -bounds.y); PIXI.CanvasGraphics.renderGraphics(this, canvasBuffer.context); return texture; }; /** * Renders the object using the WebGL renderer * * @method _renderWebGL * @param renderSession {RenderSession} * @private */ PIXI.Graphics.prototype._renderWebGL = function(renderSession) { // if the sprite is not visible or the alpha is 0 then no need to render this element if (this.visible === false || this.alpha === 0 || this.isMask === true) return; if (this._cacheAsBitmap) { if (this.dirty || this.cachedSpriteDirty) { this._generateCachedSprite(); // we will also need to update the texture on the gpu too! this.updateCachedSpriteTexture(); this.cachedSpriteDirty = false; this.dirty = false; } this._cachedSprite.worldAlpha = this.worldAlpha; PIXI.Sprite.prototype._renderWebGL.call(this._cachedSprite, renderSession); return; } else { renderSession.spriteBatch.stop(); renderSession.blendModeManager.setBlendMode(this.blendMode); if (this._mask) renderSession.maskManager.pushMask(this._mask, renderSession); if (this._filters) renderSession.filterManager.pushFilter(this._filterBlock); // check blend mode if (this.blendMode !== renderSession.spriteBatch.currentBlendMode) { renderSession.spriteBatch.currentBlendMode = this.blendMode; var blendModeWebGL = PIXI.blendModesWebGL[renderSession.spriteBatch.currentBlendMode]; renderSession.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); } // check if the webgl graphic needs to be updated if (this.webGLDirty) { this.dirty = true; this.webGLDirty = false; } PIXI.WebGLGraphics.renderGraphics(this, renderSession); // only render if it has children! if (this.children.length) { renderSession.spriteBatch.start(); // simple render children! for (var i = 0; i < this.children.length; i++) { this.children[i]._renderWebGL(renderSession); } renderSession.spriteBatch.stop(); } if (this._filters) renderSession.filterManager.popFilter(); if (this._mask) renderSession.maskManager.popMask(this.mask, renderSession); renderSession.drawCount++; renderSession.spriteBatch.start(); } }; /** * Renders the object using the Canvas renderer * * @method _renderCanvas * @param renderSession {RenderSession} * @private */ PIXI.Graphics.prototype._renderCanvas = function(renderSession) { // if the sprite is not visible or the alpha is 0 then no need to render this element if (this.visible === false || this.alpha === 0 || this.isMask === true) return; // if the tint has changed, set the graphics object to dirty. if (this._prevTint !== this.tint) { this.dirty = true; this._prevTint = this.tint; } if (this._cacheAsBitmap) { if (this.dirty || this.cachedSpriteDirty) { this._generateCachedSprite(); // we will also need to update the texture this.updateCachedSpriteTexture(); this.cachedSpriteDirty = false; this.dirty = false; } this._cachedSprite.alpha = this.alpha; PIXI.Sprite.prototype._renderCanvas.call(this._cachedSprite, renderSession); return; } else { var context = renderSession.context; var transform = this.worldTransform; if (this.blendMode !== renderSession.currentBlendMode) { renderSession.currentBlendMode = this.blendMode; context.globalCompositeOperation = PIXI.blendModesCanvas[renderSession.currentBlendMode]; } if (this._mask) { renderSession.maskManager.pushMask(this._mask, renderSession); } var resolution = renderSession.resolution; var tx = (transform.tx * renderSession.resolution) + renderSession.shakeX; var ty = (transform.ty * renderSession.resolution) + renderSession.shakeY; context.setTransform(transform.a * resolution, transform.b * resolution, transform.c * resolution, transform.d * resolution, tx, ty); PIXI.CanvasGraphics.renderGraphics(this, context); // simple render children! for (var i = 0; i < this.children.length; i++) { this.children[i]._renderCanvas(renderSession); } if (this._mask) { renderSession.maskManager.popMask(renderSession); } } }; /** * Retrieves the bounds of the graphic shape as a rectangle object * * @method getBounds * @return {Rectangle} the rectangular bounding area */ PIXI.Graphics.prototype.getBounds = function(matrix) { if (!this._currentBounds) { // Return an empty object if the item is a mask! if (!this.renderable) { return PIXI.EmptyRectangle; } if (this.dirty) { this.updateLocalBounds(); this.webGLDirty = true; this.cachedSpriteDirty = true; this.dirty = 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; }; /** * Retrieves the non-global local bounds of the graphic shape as a rectangle. The calculation takes all visible children into consideration. * * @method getLocalBounds * @return {Rectangle} The rectangular bounding area */ PIXI.Graphics.prototype.getLocalBounds = function () { var matrixCache = this.worldTransform; this.worldTransform = PIXI.identityMatrix; for (var i = 0; i < this.children.length; i++) { this.children[i].updateTransform(); } var bounds = this.getBounds(); this.worldTransform = matrixCache; for (i = 0; i < this.children.length; i++) { this.children[i].updateTransform(); } return bounds; }; /** * Tests if a point is inside this graphics object * * @param point {Point} the point to test * @return {boolean} the result of the test */ PIXI.Graphics.prototype.containsPoint = function( 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 * * @method updateLocalBounds */ PIXI.Graphics.prototype.updateLocalBounds = function() { 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 === PIXI.Graphics.RECT || type === PIXI.Graphics.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 === PIXI.Graphics.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 === PIXI.Graphics.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 - assumes points are sequential, not Point objects points = shape.points; for (var j = 0; j < points.length; j++) { if (points[j] instanceof Phaser.Point) { x = points[j].x; y = points[j].y; } else { x = points[j]; y = points[j + 1]; if (j < points.length - 1) { j++; } } 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; }; /** * Generates the cached sprite when the sprite has cacheAsBitmap = true * * @method _generateCachedSprite * @private */ PIXI.Graphics.prototype._generateCachedSprite = function() { var bounds = this.getLocalBounds(); if (!this._cachedSprite) { var canvasBuffer = new PIXI.CanvasBuffer(bounds.width, bounds.height); var texture = PIXI.Texture.fromCanvas(canvasBuffer.canvas); this._cachedSprite = new PIXI.Sprite(texture); this._cachedSprite.buffer = canvasBuffer; this._cachedSprite.worldTransform = this.worldTransform; } else { this._cachedSprite.buffer.resize(bounds.width, bounds.height); } // leverage the anchor to account for the offset of the element this._cachedSprite.anchor.x = -(bounds.x / bounds.width); this._cachedSprite.anchor.y = -(bounds.y / bounds.height); // this._cachedSprite.buffer.context.save(); this._cachedSprite.buffer.context.translate(-bounds.x, -bounds.y); // make sure we set the alpha of the graphics to 1 for the render.. this.worldAlpha = 1; // now render the graphic.. PIXI.CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); this._cachedSprite.alpha = this.alpha; }; /** * Updates texture size based on canvas size * * @method updateCachedSpriteTexture * @private */ PIXI.Graphics.prototype.updateCachedSpriteTexture = function() { var cachedSprite = this._cachedSprite; var texture = cachedSprite.texture; var canvas = cachedSprite.buffer.canvas; texture.baseTexture.width = canvas.width; texture.baseTexture.height = canvas.height; texture.crop.width = texture.frame.width = canvas.width; texture.crop.height = texture.frame.height = canvas.height; cachedSprite._width = canvas.width; cachedSprite._height = canvas.height; // update the dirty base textures texture.baseTexture.dirty(); }; /** * Destroys a previous cached sprite. * * @method destroyCachedSprite */ PIXI.Graphics.prototype.destroyCachedSprite = function() { this._cachedSprite.texture.destroy(true); this._cachedSprite = null; }; /** * Draws the given shape to this Graphics object. Can be any of Circle, Rectangle, Ellipse, Line or Polygon. * * @method drawShape * @param {Circle|Rectangle|Ellipse|Line|Polygon} shape The Shape object to draw. * @return {GraphicsData} The generated GraphicsData object. */ PIXI.Graphics.prototype.drawShape = function(shape) { if (this.currentPath) { // check current path! if (this.currentPath.shape.points.length <= 2) { this.graphicsData.pop(); } } this.currentPath = null; // Handle mixed-type polygons if (shape instanceof Phaser.Polygon) { shape = shape.clone(); shape.flatten(); } var data = new PIXI.GraphicsData(this.lineWidth, this.lineColor, this.lineAlpha, this.fillColor, this.fillAlpha, this.filling, shape); this.graphicsData.push(data); if (data.type === PIXI.Graphics.POLY) { data.shape.closed = this.filling; this.currentPath = data; } this.dirty = true; this._boundsDirty = true; return data; }; /** * When cacheAsBitmap is set to true the graphics object will be rendered as if it was a sprite. * This is useful if your graphics element does not change often, as it will speed up the rendering of the object in exchange for taking up texture memory. * It is also useful if you need the graphics object to be anti-aliased, because it will be rendered using canvas. * This is not recommended if you are constantly redrawing the graphics element. * * @property cacheAsBitmap * @type Boolean * @default false * @private */ Object.defineProperty(PIXI.Graphics.prototype, "cacheAsBitmap", { get: function() { return this._cacheAsBitmap; }, set: function(value) { this._cacheAsBitmap = value; if (this._cacheAsBitmap) { this._generateCachedSprite(); } else { this.destroyCachedSprite(); } this.dirty = true; this.webGLDirty = true; } }); /** * A GraphicsData object. * * @class GraphicsData * @constructor PIXI.GraphicsData = function(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) { this.lineWidth = lineWidth; this.lineColor = lineColor; this.lineAlpha = lineAlpha; this._lineTint = lineColor; this.fillColor = fillColor; this.fillAlpha = fillAlpha; this._fillTint = fillColor; this.fill = fill; this.shape = shape; this.type = shape.type; }; */ /** * A GraphicsData object. * * @class * @memberof PIXI * @param lineWidth {number} the width of the line to draw * @param lineColor {number} the color of the line to draw * @param lineAlpha {number} the alpha of the line to draw * @param fillColor {number} the color of the fill * @param fillAlpha {number} the alpha of the fill * @param fill {boolean} whether or not the shape is filled with a colour * @param shape {Circle|Rectangle|Ellipse|Line|Polygon} The shape object to draw. */ PIXI.GraphicsData = function(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, fill, shape) { /* * @member {number} the width of the line to draw */ this.lineWidth = lineWidth; /* * @member {number} the color of the line to draw */ this.lineColor = lineColor; /* * @member {number} the alpha of the line to draw */ this.lineAlpha = lineAlpha; /* * @member {number} cached tint of the line to draw */ this._lineTint = lineColor; /* * @member {number} the color of the fill */ this.fillColor = fillColor; /* * @member {number} the alpha of the fill */ this.fillAlpha = fillAlpha; /* * @member {number} cached tint of the fill */ this._fillTint = fillColor; /* * @member {boolean} whether or not the shape is filled with a color */ this.fill = fill; /* * @member {Circle|Rectangle|Ellipse|Line|Polygon} The shape object to draw. */ this.shape = shape; /* * @member {number} The type of the shape, see the Const.Shapes file for all the existing types, */ this.type = shape.type; }; PIXI.GraphicsData.prototype.constructor = PIXI.GraphicsData; /** * Creates a new GraphicsData object with the same values as this one. * * @return {GraphicsData} */ PIXI.GraphicsData.prototype.clone = function() { return new GraphicsData( this.lineWidth, this.lineColor, this.lineAlpha, this.fillColor, this.fillAlpha, this.fill, this.shape ); }; /* Copyright (c) 2016, Mapbox Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /** * @class EarCut */ PIXI.EarCut = {}; PIXI.EarCut.Triangulate = function (data, holeIndices, dim) { dim = dim || 2; var hasHoles = holeIndices && holeIndices.length, outerLen = hasHoles ? holeIndices[0] * dim : data.length, outerNode = PIXI.EarCut.linkedList(data, 0, outerLen, dim, true), triangles = []; if (!outerNode) return triangles; var minX, minY, maxX, maxY, x, y, size; if (hasHoles) outerNode = PIXI.EarCut.eliminateHoles(data, holeIndices, outerNode, dim); // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox if (data.length > 80 * dim) { minX = maxX = data[0]; minY = maxY = data[1]; for (var i = dim; i < outerLen; i += dim) { x = data[i]; y = data[i + 1]; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } // minX, minY and size are later used to transform coords into integers for z-order calculation size = Math.max(maxX - minX, maxY - minY); } PIXI.EarCut.earcutLinked(outerNode, triangles, dim, minX, minY, size); return triangles; } // create a circular doubly linked list from polygon points in the specified winding order PIXI.EarCut.linkedList = function (data, start, end, dim, clockwise) { var sum = 0, i, j, last; // calculate original winding order of a polygon ring for (i = start, j = end - dim; i < end; i += dim) { sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); j = i; } // link points into circular doubly-linked list in the specified winding order if (clockwise === (sum > 0)) { for (i = start; i < end; i += dim) last = PIXI.EarCut.insertNode(i, data[i], data[i + 1], last); } else { for (i = end - dim; i >= start; i -= dim) last = PIXI.EarCut.insertNode(i, data[i], data[i + 1], last); } return last; } // eliminate colinear or duplicate points PIXI.EarCut.filterPoints = function (start, end) { if (!start) return start; if (!end) end = start; var p = start, again; do { again = false; if (!p.steiner && (PIXI.EarCut.equals(p, p.next) || PIXI.EarCut.area(p.prev, p, p.next) === 0)) { PIXI.EarCut.removeNode(p); p = end = p.prev; if (p === p.next) return null; again = true; } else { p = p.next; } } while (again || p !== end); return end; } // main ear slicing loop which triangulates a polygon (given as a linked list) PIXI.EarCut.earcutLinked = function (ear, triangles, dim, minX, minY, size, pass) { if (!ear) return; // interlink polygon nodes in z-order if (!pass && size) PIXI.EarCut.indexCurve(ear, minX, minY, size); var stop = ear, prev, next; // iterate through ears, slicing them one by one while (ear.prev !== ear.next) { prev = ear.prev; next = ear.next; if (size ? PIXI.EarCut.isEarHashed(ear, minX, minY, size) : PIXI.EarCut.isEar(ear)) { // cut off the triangle triangles.push(prev.i / dim); triangles.push(ear.i / dim); triangles.push(next.i / dim); PIXI.EarCut.removeNode(ear); // skipping the next vertice leads to less sliver triangles ear = next.next; stop = next.next; continue; } ear = next; // if we looped through the whole remaining polygon and can't find any more ears if (ear === stop) { // try filtering points and slicing again if (!pass) { PIXI.EarCut.earcutLinked(PIXI.EarCut.filterPoints(ear), triangles, dim, minX, minY, size, 1); // if this didn't work, try curing all small self-intersections locally } else if (pass === 1) { ear = PIXI.EarCut.cureLocalIntersections(ear, triangles, dim); PIXI.EarCut.earcutLinked(ear, triangles, dim, minX, minY, size, 2); // as a last resort, try splitting the remaining polygon into two } else if (pass === 2) { PIXI.EarCut.splitEarcut(ear, triangles, dim, minX, minY, size); } break; } } } // check whether a polygon node forms a valid ear with adjacent nodes PIXI.EarCut.isEar = function (ear) { var a = ear.prev, b = ear, c = ear.next; if (PIXI.EarCut.area(a, b, c) >= 0) return false; // reflex, can't be an ear // now make sure we don't have other points inside the potential ear var p = ear.next.next; while (p !== ear.prev) { if (PIXI.EarCut.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && PIXI.EarCut.area(p.prev, p, p.next) >= 0) return false; p = p.next; } return true; } PIXI.EarCut.isEarHashed = function (ear, minX, minY, size) { var a = ear.prev, b = ear, c = ear.next; if (PIXI.EarCut.area(a, b, c) >= 0) return false; // reflex, can't be an ear // triangle bbox; min & max are calculated like this for speed var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x), minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y), maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x), maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); // z-order range for the current triangle bbox; var minZ = PIXI.EarCut.zOrder(minTX, minTY, minX, minY, size), maxZ = PIXI.EarCut.zOrder(maxTX, maxTY, minX, minY, size); // first look for points inside the triangle in increasing z-order var p = ear.nextZ; while (p && p.z <= maxZ) { if (p !== ear.prev && p !== ear.next && PIXI.EarCut.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && PIXI.EarCut.area(p.prev, p, p.next) >= 0) return false; p = p.nextZ; } // then look for points in decreasing z-order p = ear.prevZ; while (p && p.z >= minZ) { if (p !== ear.prev && p !== ear.next && PIXI.EarCut.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && PIXI.EarCut.area(p.prev, p, p.next) >= 0) return false; p = p.prevZ; } return true; } // go through all polygon nodes and cure small local self-intersections PIXI.EarCut.cureLocalIntersections = function (start, triangles, dim) { var p = start; do { var a = p.prev, b = p.next.next; // a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2]) if (PIXI.EarCut.intersects(a, p, p.next, b) && PIXI.EarCut.locallyInside(a, b) && PIXI.EarCut.locallyInside(b, a)) { triangles.push(a.i / dim); triangles.push(p.i / dim); triangles.push(b.i / dim); // remove two nodes involved PIXI.EarCut.removeNode(p); PIXI.EarCut.removeNode(p.next); p = start = b; } p = p.next; } while (p !== start); return p; } // try splitting polygon into two and triangulate them independently PIXI.EarCut.splitEarcut = function (start, triangles, dim, minX, minY, size) { // look for a valid diagonal that divides the polygon into two var a = start; do { var b = a.next.next; while (b !== a.prev) { if (a.i !== b.i && PIXI.EarCut.isValidDiagonal(a, b)) { // split the polygon in two by the diagonal var c = PIXI.EarCut.splitPolygon(a, b); // filter colinear points around the cuts a = PIXI.EarCut.filterPoints(a, a.next); c = PIXI.EarCut.filterPoints(c, c.next); // run earcut on each half PIXI.EarCut.earcutLinked(a, triangles, dim, minX, minY, size); PIXI.EarCut.earcutLinked(c, triangles, dim, minX, minY, size); return; } b = b.next; } a = a.next; } while (a !== start); } // link every hole into the outer loop, producing a single-ring polygon without holes PIXI.EarCut.eliminateHoles = function (data, holeIndices, outerNode, dim) { var queue = [], i, len, start, end, list; for (i = 0, len = holeIndices.length; i < len; i++) { start = holeIndices[i] * dim; end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; list = PIXI.EarCut.linkedList(data, start, end, dim, false); if (list === list.next) list.steiner = true; queue.push(PIXI.EarCut.getLeftmost(list)); } queue.sort(compareX); // process holes from left to right for (i = 0; i < queue.length; i++) { PIXI.EarCut.eliminateHole(queue[i], outerNode); outerNode = PIXI.EarCut.filterPoints(outerNode, outerNode.next); } return outerNode; } PIXI.EarCut.compareX = function (a, b) { return a.x - b.x; } // find a bridge between vertices that connects hole with an outer ring and and link it PIXI.EarCut.eliminateHole = function (hole, outerNode) { outerNode = PIXI.EarCut.findHoleBridge(hole, outerNode); if (outerNode) { var b = PIXI.EarCut.splitPolygon(outerNode, hole); PIXI.EarCut.filterPoints(b, b.next); } } // David Eberly's algorithm for finding a bridge between hole and outer polygon PIXI.EarCut.findHoleBridge = function (hole, outerNode) { var p = outerNode, hx = hole.x, hy = hole.y, qx = -Infinity, m; // find a segment intersected by a ray from the hole's leftmost point to the left; // segment's endpoint with lesser x will be potential connection point do { if (hy <= p.y && hy >= p.next.y) { var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); if (x <= hx && x > qx) { qx = x; m = p.x < p.next.x ? p : p.next; } } p = p.next; } while (p !== outerNode); if (!m) return null; if (hole.x === m.x) return m.prev; // hole touches outer segment; pick lower endpoint // look for points inside the triangle of hole point, segment intersection and endpoint; // if there are no points found, we have a valid connection; // otherwise choose the point of the minimum angle with the ray as connection point var stop = m, tanMin = Infinity, tan; p