diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/renderers/canvas/CanvasGraphics.js b/src/core/renderers/canvas/CanvasGraphics.js deleted file mode 100644 index ef7e765..0000000 --- a/src/core/renderers/canvas/CanvasGraphics.js +++ /dev/null @@ -1,318 +0,0 @@ -var CONST = require('../../const'); - -/** - * A set of functions used by the canvas renderer to draw the primitive graphics data. - * - * @namespace PIXI - */ -var CanvasGraphics = module.exports = {}; - -/* - * Renders a Graphics object to a canvas. - * - * @param graphics {Graphics} the actual graphics object to render - * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas - */ -CanvasGraphics.renderGraphics = function (graphics, context) { - var worldAlpha = graphics.worldAlpha; - - if (graphics.dirty) { - this.updateGraphicsTint(graphics); - graphics.dirty = false; - } - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - var fillColor = data._fillTint; - var lineColor = data._lineTint; - - context.lineWidth = data.lineWidth; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - if (shape.closed) { - context.lineTo(points[0], points[1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RECT) { - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fillRect(shape.x, shape.y, shape.width, shape.height); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.strokeRect(shape.x, shape.y, shape.width, shape.height); - } - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.ELIP) { - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RREC) { - var rx = shape.x; - var ry = shape.y; - var width = shape.width; - var height = shape.height; - var radius = shape.radius; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - } -}; - -/* - * Renders a graphics mask - * - * @private - * @param graphics {Graphics} the graphics which will be used as a mask - * @param context {CanvasRenderingContext2D} the context 2d method of the canvas - */ -CanvasGraphics.renderGraphicsMask = function (graphics, context) { - var len = graphics.graphicsData.length; - - if (len === 0) { - return; - } - - if (len > 1) { - len = 1; - window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); - } - - for (var i = 0; i < 1; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - } - else if (data.type === CONST.SHAPES.RECT) { - context.beginPath(); - context.rect(shape.x, shape.y, shape.width, shape.height); - context.closePath(); - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - } - else if (data.type === CONST.SHAPES.ELIP) { - - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - context.closePath(); - } - else if (data.type === CONST.SHAPES.RREC) { - - var pts = shape.points; - var rx = pts[0]; - var ry = pts[1]; - var width = pts[2]; - var height = pts[3]; - var radius = pts[4]; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - } - } -}; - -CanvasGraphics.updateGraphicsTint = function (graphics) { - if (graphics.tint === 0xFFFFFF) { - return; - } - - var tintR = (graphics.tint >> 16 & 0xFF) / 255; - var tintG = (graphics.tint >> 8 & 0xFF) / 255; - var tintB = (graphics.tint & 0xFF)/ 255; - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - - var fillColor = data.fillColor | 0; - var lineColor = data.lineColor | 0; - - /* - var colorR = (fillColor >> 16 & 0xFF) / 255; - var colorG = (fillColor >> 8 & 0xFF) / 255; - var colorB = (fillColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - - colorR = (lineColor >> 16 & 0xFF) / 255; - colorG = (lineColor >> 8 & 0xFF) / 255; - colorB = (lineColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - */ - - // super inline cos im an optimization NAZI :) - data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); - data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); - - } -}; - diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/renderers/canvas/CanvasGraphics.js b/src/core/renderers/canvas/CanvasGraphics.js deleted file mode 100644 index ef7e765..0000000 --- a/src/core/renderers/canvas/CanvasGraphics.js +++ /dev/null @@ -1,318 +0,0 @@ -var CONST = require('../../const'); - -/** - * A set of functions used by the canvas renderer to draw the primitive graphics data. - * - * @namespace PIXI - */ -var CanvasGraphics = module.exports = {}; - -/* - * Renders a Graphics object to a canvas. - * - * @param graphics {Graphics} the actual graphics object to render - * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas - */ -CanvasGraphics.renderGraphics = function (graphics, context) { - var worldAlpha = graphics.worldAlpha; - - if (graphics.dirty) { - this.updateGraphicsTint(graphics); - graphics.dirty = false; - } - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - var fillColor = data._fillTint; - var lineColor = data._lineTint; - - context.lineWidth = data.lineWidth; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - if (shape.closed) { - context.lineTo(points[0], points[1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RECT) { - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fillRect(shape.x, shape.y, shape.width, shape.height); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.strokeRect(shape.x, shape.y, shape.width, shape.height); - } - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.ELIP) { - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RREC) { - var rx = shape.x; - var ry = shape.y; - var width = shape.width; - var height = shape.height; - var radius = shape.radius; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - } -}; - -/* - * Renders a graphics mask - * - * @private - * @param graphics {Graphics} the graphics which will be used as a mask - * @param context {CanvasRenderingContext2D} the context 2d method of the canvas - */ -CanvasGraphics.renderGraphicsMask = function (graphics, context) { - var len = graphics.graphicsData.length; - - if (len === 0) { - return; - } - - if (len > 1) { - len = 1; - window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); - } - - for (var i = 0; i < 1; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - } - else if (data.type === CONST.SHAPES.RECT) { - context.beginPath(); - context.rect(shape.x, shape.y, shape.width, shape.height); - context.closePath(); - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - } - else if (data.type === CONST.SHAPES.ELIP) { - - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - context.closePath(); - } - else if (data.type === CONST.SHAPES.RREC) { - - var pts = shape.points; - var rx = pts[0]; - var ry = pts[1]; - var width = pts[2]; - var height = pts[3]; - var radius = pts[4]; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - } - } -}; - -CanvasGraphics.updateGraphicsTint = function (graphics) { - if (graphics.tint === 0xFFFFFF) { - return; - } - - var tintR = (graphics.tint >> 16 & 0xFF) / 255; - var tintG = (graphics.tint >> 8 & 0xFF) / 255; - var tintB = (graphics.tint & 0xFF)/ 255; - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - - var fillColor = data.fillColor | 0; - var lineColor = data.lineColor | 0; - - /* - var colorR = (fillColor >> 16 & 0xFF) / 255; - var colorG = (fillColor >> 8 & 0xFF) / 255; - var colorB = (fillColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - - colorR = (lineColor >> 16 & 0xFF) / 255; - colorG = (lineColor >> 8 & 0xFF) / 255; - colorB = (lineColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - */ - - // super inline cos im an optimization NAZI :) - data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); - data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); - - } -}; - diff --git a/src/core/renderers/canvas/utils/CanvasGraphics.js b/src/core/renderers/canvas/utils/CanvasGraphics.js new file mode 100644 index 0000000..ef7e765 --- /dev/null +++ b/src/core/renderers/canvas/utils/CanvasGraphics.js @@ -0,0 +1,318 @@ +var CONST = require('../../const'); + +/** + * A set of functions used by the canvas renderer to draw the primitive graphics data. + * + * @namespace PIXI + */ +var CanvasGraphics = module.exports = {}; + +/* + * Renders a Graphics object to a canvas. + * + * @param graphics {Graphics} the actual graphics object to render + * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas + */ +CanvasGraphics.renderGraphics = function (graphics, context) { + var worldAlpha = graphics.worldAlpha; + + if (graphics.dirty) { + this.updateGraphicsTint(graphics); + graphics.dirty = false; + } + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + var fillColor = data._fillTint; + var lineColor = data._lineTint; + + context.lineWidth = data.lineWidth; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + if (shape.closed) { + context.lineTo(points[0], points[1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RECT) { + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fillRect(shape.x, shape.y, shape.width, shape.height); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.strokeRect(shape.x, shape.y, shape.width, shape.height); + } + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.ELIP) { + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RREC) { + var rx = shape.x; + var ry = shape.y; + var width = shape.width; + var height = shape.height; + var radius = shape.radius; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + } +}; + +/* + * Renders a graphics mask + * + * @private + * @param graphics {Graphics} the graphics which will be used as a mask + * @param context {CanvasRenderingContext2D} the context 2d method of the canvas + */ +CanvasGraphics.renderGraphicsMask = function (graphics, context) { + var len = graphics.graphicsData.length; + + if (len === 0) { + return; + } + + if (len > 1) { + len = 1; + window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); + } + + for (var i = 0; i < 1; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + } + else if (data.type === CONST.SHAPES.RECT) { + context.beginPath(); + context.rect(shape.x, shape.y, shape.width, shape.height); + context.closePath(); + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + } + else if (data.type === CONST.SHAPES.ELIP) { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + context.closePath(); + } + else if (data.type === CONST.SHAPES.RREC) { + + var pts = shape.points; + var rx = pts[0]; + var ry = pts[1]; + var width = pts[2]; + var height = pts[3]; + var radius = pts[4]; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + } + } +}; + +CanvasGraphics.updateGraphicsTint = function (graphics) { + if (graphics.tint === 0xFFFFFF) { + return; + } + + var tintR = (graphics.tint >> 16 & 0xFF) / 255; + var tintG = (graphics.tint >> 8 & 0xFF) / 255; + var tintB = (graphics.tint & 0xFF)/ 255; + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + + var fillColor = data.fillColor | 0; + var lineColor = data.lineColor | 0; + + /* + var colorR = (fillColor >> 16 & 0xFF) / 255; + var colorG = (fillColor >> 8 & 0xFF) / 255; + var colorB = (fillColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + + colorR = (lineColor >> 16 & 0xFF) / 255; + colorG = (lineColor >> 8 & 0xFF) / 255; + colorB = (lineColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + */ + + // super inline cos im an optimization NAZI :) + data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); + data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); + + } +}; + diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/renderers/canvas/CanvasGraphics.js b/src/core/renderers/canvas/CanvasGraphics.js deleted file mode 100644 index ef7e765..0000000 --- a/src/core/renderers/canvas/CanvasGraphics.js +++ /dev/null @@ -1,318 +0,0 @@ -var CONST = require('../../const'); - -/** - * A set of functions used by the canvas renderer to draw the primitive graphics data. - * - * @namespace PIXI - */ -var CanvasGraphics = module.exports = {}; - -/* - * Renders a Graphics object to a canvas. - * - * @param graphics {Graphics} the actual graphics object to render - * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas - */ -CanvasGraphics.renderGraphics = function (graphics, context) { - var worldAlpha = graphics.worldAlpha; - - if (graphics.dirty) { - this.updateGraphicsTint(graphics); - graphics.dirty = false; - } - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - var fillColor = data._fillTint; - var lineColor = data._lineTint; - - context.lineWidth = data.lineWidth; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - if (shape.closed) { - context.lineTo(points[0], points[1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RECT) { - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fillRect(shape.x, shape.y, shape.width, shape.height); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.strokeRect(shape.x, shape.y, shape.width, shape.height); - } - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.ELIP) { - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RREC) { - var rx = shape.x; - var ry = shape.y; - var width = shape.width; - var height = shape.height; - var radius = shape.radius; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - } -}; - -/* - * Renders a graphics mask - * - * @private - * @param graphics {Graphics} the graphics which will be used as a mask - * @param context {CanvasRenderingContext2D} the context 2d method of the canvas - */ -CanvasGraphics.renderGraphicsMask = function (graphics, context) { - var len = graphics.graphicsData.length; - - if (len === 0) { - return; - } - - if (len > 1) { - len = 1; - window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); - } - - for (var i = 0; i < 1; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - } - else if (data.type === CONST.SHAPES.RECT) { - context.beginPath(); - context.rect(shape.x, shape.y, shape.width, shape.height); - context.closePath(); - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - } - else if (data.type === CONST.SHAPES.ELIP) { - - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - context.closePath(); - } - else if (data.type === CONST.SHAPES.RREC) { - - var pts = shape.points; - var rx = pts[0]; - var ry = pts[1]; - var width = pts[2]; - var height = pts[3]; - var radius = pts[4]; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - } - } -}; - -CanvasGraphics.updateGraphicsTint = function (graphics) { - if (graphics.tint === 0xFFFFFF) { - return; - } - - var tintR = (graphics.tint >> 16 & 0xFF) / 255; - var tintG = (graphics.tint >> 8 & 0xFF) / 255; - var tintB = (graphics.tint & 0xFF)/ 255; - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - - var fillColor = data.fillColor | 0; - var lineColor = data.lineColor | 0; - - /* - var colorR = (fillColor >> 16 & 0xFF) / 255; - var colorG = (fillColor >> 8 & 0xFF) / 255; - var colorB = (fillColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - - colorR = (lineColor >> 16 & 0xFF) / 255; - colorG = (lineColor >> 8 & 0xFF) / 255; - colorB = (lineColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - */ - - // super inline cos im an optimization NAZI :) - data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); - data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); - - } -}; - diff --git a/src/core/renderers/canvas/utils/CanvasGraphics.js b/src/core/renderers/canvas/utils/CanvasGraphics.js new file mode 100644 index 0000000..ef7e765 --- /dev/null +++ b/src/core/renderers/canvas/utils/CanvasGraphics.js @@ -0,0 +1,318 @@ +var CONST = require('../../const'); + +/** + * A set of functions used by the canvas renderer to draw the primitive graphics data. + * + * @namespace PIXI + */ +var CanvasGraphics = module.exports = {}; + +/* + * Renders a Graphics object to a canvas. + * + * @param graphics {Graphics} the actual graphics object to render + * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas + */ +CanvasGraphics.renderGraphics = function (graphics, context) { + var worldAlpha = graphics.worldAlpha; + + if (graphics.dirty) { + this.updateGraphicsTint(graphics); + graphics.dirty = false; + } + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + var fillColor = data._fillTint; + var lineColor = data._lineTint; + + context.lineWidth = data.lineWidth; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + if (shape.closed) { + context.lineTo(points[0], points[1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RECT) { + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fillRect(shape.x, shape.y, shape.width, shape.height); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.strokeRect(shape.x, shape.y, shape.width, shape.height); + } + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.ELIP) { + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RREC) { + var rx = shape.x; + var ry = shape.y; + var width = shape.width; + var height = shape.height; + var radius = shape.radius; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + } +}; + +/* + * Renders a graphics mask + * + * @private + * @param graphics {Graphics} the graphics which will be used as a mask + * @param context {CanvasRenderingContext2D} the context 2d method of the canvas + */ +CanvasGraphics.renderGraphicsMask = function (graphics, context) { + var len = graphics.graphicsData.length; + + if (len === 0) { + return; + } + + if (len > 1) { + len = 1; + window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); + } + + for (var i = 0; i < 1; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + } + else if (data.type === CONST.SHAPES.RECT) { + context.beginPath(); + context.rect(shape.x, shape.y, shape.width, shape.height); + context.closePath(); + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + } + else if (data.type === CONST.SHAPES.ELIP) { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + context.closePath(); + } + else if (data.type === CONST.SHAPES.RREC) { + + var pts = shape.points; + var rx = pts[0]; + var ry = pts[1]; + var width = pts[2]; + var height = pts[3]; + var radius = pts[4]; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + } + } +}; + +CanvasGraphics.updateGraphicsTint = function (graphics) { + if (graphics.tint === 0xFFFFFF) { + return; + } + + var tintR = (graphics.tint >> 16 & 0xFF) / 255; + var tintG = (graphics.tint >> 8 & 0xFF) / 255; + var tintB = (graphics.tint & 0xFF)/ 255; + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + + var fillColor = data.fillColor | 0; + var lineColor = data.lineColor | 0; + + /* + var colorR = (fillColor >> 16 & 0xFF) / 255; + var colorG = (fillColor >> 8 & 0xFF) / 255; + var colorB = (fillColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + + colorR = (lineColor >> 16 & 0xFF) / 255; + colorG = (lineColor >> 8 & 0xFF) / 255; + colorB = (lineColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + */ + + // super inline cos im an optimization NAZI :) + data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); + data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); + + } +}; + diff --git a/src/core/renderers/canvas/utils/CanvasMaskManager.js b/src/core/renderers/canvas/utils/CanvasMaskManager.js index 61ada5f..e04a733 100644 --- a/src/core/renderers/canvas/utils/CanvasMaskManager.js +++ b/src/core/renderers/canvas/utils/CanvasMaskManager.js @@ -1,4 +1,4 @@ -var CanvasGraphics = require('../CanvasGraphics'); +var CanvasGraphics = require('./CanvasGraphics'); /** * A set of functions used to handle masking. diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/renderers/canvas/CanvasGraphics.js b/src/core/renderers/canvas/CanvasGraphics.js deleted file mode 100644 index ef7e765..0000000 --- a/src/core/renderers/canvas/CanvasGraphics.js +++ /dev/null @@ -1,318 +0,0 @@ -var CONST = require('../../const'); - -/** - * A set of functions used by the canvas renderer to draw the primitive graphics data. - * - * @namespace PIXI - */ -var CanvasGraphics = module.exports = {}; - -/* - * Renders a Graphics object to a canvas. - * - * @param graphics {Graphics} the actual graphics object to render - * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas - */ -CanvasGraphics.renderGraphics = function (graphics, context) { - var worldAlpha = graphics.worldAlpha; - - if (graphics.dirty) { - this.updateGraphicsTint(graphics); - graphics.dirty = false; - } - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - var fillColor = data._fillTint; - var lineColor = data._lineTint; - - context.lineWidth = data.lineWidth; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - if (shape.closed) { - context.lineTo(points[0], points[1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RECT) { - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fillRect(shape.x, shape.y, shape.width, shape.height); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.strokeRect(shape.x, shape.y, shape.width, shape.height); - } - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.ELIP) { - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RREC) { - var rx = shape.x; - var ry = shape.y; - var width = shape.width; - var height = shape.height; - var radius = shape.radius; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - } -}; - -/* - * Renders a graphics mask - * - * @private - * @param graphics {Graphics} the graphics which will be used as a mask - * @param context {CanvasRenderingContext2D} the context 2d method of the canvas - */ -CanvasGraphics.renderGraphicsMask = function (graphics, context) { - var len = graphics.graphicsData.length; - - if (len === 0) { - return; - } - - if (len > 1) { - len = 1; - window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); - } - - for (var i = 0; i < 1; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - } - else if (data.type === CONST.SHAPES.RECT) { - context.beginPath(); - context.rect(shape.x, shape.y, shape.width, shape.height); - context.closePath(); - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - } - else if (data.type === CONST.SHAPES.ELIP) { - - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - context.closePath(); - } - else if (data.type === CONST.SHAPES.RREC) { - - var pts = shape.points; - var rx = pts[0]; - var ry = pts[1]; - var width = pts[2]; - var height = pts[3]; - var radius = pts[4]; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - } - } -}; - -CanvasGraphics.updateGraphicsTint = function (graphics) { - if (graphics.tint === 0xFFFFFF) { - return; - } - - var tintR = (graphics.tint >> 16 & 0xFF) / 255; - var tintG = (graphics.tint >> 8 & 0xFF) / 255; - var tintB = (graphics.tint & 0xFF)/ 255; - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - - var fillColor = data.fillColor | 0; - var lineColor = data.lineColor | 0; - - /* - var colorR = (fillColor >> 16 & 0xFF) / 255; - var colorG = (fillColor >> 8 & 0xFF) / 255; - var colorB = (fillColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - - colorR = (lineColor >> 16 & 0xFF) / 255; - colorG = (lineColor >> 8 & 0xFF) / 255; - colorB = (lineColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - */ - - // super inline cos im an optimization NAZI :) - data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); - data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); - - } -}; - diff --git a/src/core/renderers/canvas/utils/CanvasGraphics.js b/src/core/renderers/canvas/utils/CanvasGraphics.js new file mode 100644 index 0000000..ef7e765 --- /dev/null +++ b/src/core/renderers/canvas/utils/CanvasGraphics.js @@ -0,0 +1,318 @@ +var CONST = require('../../const'); + +/** + * A set of functions used by the canvas renderer to draw the primitive graphics data. + * + * @namespace PIXI + */ +var CanvasGraphics = module.exports = {}; + +/* + * Renders a Graphics object to a canvas. + * + * @param graphics {Graphics} the actual graphics object to render + * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas + */ +CanvasGraphics.renderGraphics = function (graphics, context) { + var worldAlpha = graphics.worldAlpha; + + if (graphics.dirty) { + this.updateGraphicsTint(graphics); + graphics.dirty = false; + } + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + var fillColor = data._fillTint; + var lineColor = data._lineTint; + + context.lineWidth = data.lineWidth; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + if (shape.closed) { + context.lineTo(points[0], points[1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RECT) { + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fillRect(shape.x, shape.y, shape.width, shape.height); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.strokeRect(shape.x, shape.y, shape.width, shape.height); + } + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.ELIP) { + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RREC) { + var rx = shape.x; + var ry = shape.y; + var width = shape.width; + var height = shape.height; + var radius = shape.radius; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + } +}; + +/* + * Renders a graphics mask + * + * @private + * @param graphics {Graphics} the graphics which will be used as a mask + * @param context {CanvasRenderingContext2D} the context 2d method of the canvas + */ +CanvasGraphics.renderGraphicsMask = function (graphics, context) { + var len = graphics.graphicsData.length; + + if (len === 0) { + return; + } + + if (len > 1) { + len = 1; + window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); + } + + for (var i = 0; i < 1; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + } + else if (data.type === CONST.SHAPES.RECT) { + context.beginPath(); + context.rect(shape.x, shape.y, shape.width, shape.height); + context.closePath(); + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + } + else if (data.type === CONST.SHAPES.ELIP) { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + context.closePath(); + } + else if (data.type === CONST.SHAPES.RREC) { + + var pts = shape.points; + var rx = pts[0]; + var ry = pts[1]; + var width = pts[2]; + var height = pts[3]; + var radius = pts[4]; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + } + } +}; + +CanvasGraphics.updateGraphicsTint = function (graphics) { + if (graphics.tint === 0xFFFFFF) { + return; + } + + var tintR = (graphics.tint >> 16 & 0xFF) / 255; + var tintG = (graphics.tint >> 8 & 0xFF) / 255; + var tintB = (graphics.tint & 0xFF)/ 255; + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + + var fillColor = data.fillColor | 0; + var lineColor = data.lineColor | 0; + + /* + var colorR = (fillColor >> 16 & 0xFF) / 255; + var colorG = (fillColor >> 8 & 0xFF) / 255; + var colorB = (fillColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + + colorR = (lineColor >> 16 & 0xFF) / 255; + colorG = (lineColor >> 8 & 0xFF) / 255; + colorB = (lineColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + */ + + // super inline cos im an optimization NAZI :) + data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); + data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); + + } +}; + diff --git a/src/core/renderers/canvas/utils/CanvasMaskManager.js b/src/core/renderers/canvas/utils/CanvasMaskManager.js index 61ada5f..e04a733 100644 --- a/src/core/renderers/canvas/utils/CanvasMaskManager.js +++ b/src/core/renderers/canvas/utils/CanvasMaskManager.js @@ -1,4 +1,4 @@ -var CanvasGraphics = require('../CanvasGraphics'); +var CanvasGraphics = require('./CanvasGraphics'); /** * A set of functions used to handle masking. diff --git a/src/primitives/Graphics.js b/src/primitives/Graphics.js deleted file mode 100644 index 665eede..0000000 --- a/src/primitives/Graphics.js +++ /dev/null @@ -1,1026 +0,0 @@ -var core = require('../core'), - GraphicsData = require('./GraphicsData'); - -/** - * 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 - * @extends DisplayObjectContainer - * @namespace PIXI - */ -function Graphics() { - core.DisplayObjectContainer.call(this); - - this.renderable = true; - - /** - * 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 {object[]} - * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. - * - * @member {number} - * @default CONST.blendModes.NORMAL; - */ - this.blendMode = core.CONST.blendModes.NORMAL; - - /** - * Current path - * - * @member {object} - * @private - */ - this.currentPath = null; - - /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object[]} - * @private - */ - 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 core.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; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; -} - -// constructor -Graphics.prototype = Object.create(core.DisplayObjectContainer.prototype); -Graphics.prototype.constructor = Graphics; -module.exports = Graphics; - -Object.defineProperties(Graphics.prototype, { - /** - * 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. - * - * @member {boolean} - * @memberof Graphics# - * @default false - * @private - */ - cacheAsBitmap: { - get: function () { - return this._cacheAsBitmap; - }, - set: function (value) { - this._cacheAsBitmap = value; - - if (this._cacheAsBitmap) { - this._generateCachedSprite(); - } - else { - this.destroyCachedSprite(); - this.dirty = true; - } - } - } -}); - -/** - * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. - * - * @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} - */ -Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { - this.lineWidth = lineWidth || 0; - this.lineColor = color || 0; - this.lineAlpha = (arguments.length < 3) ? 1 : alpha; - - if (this.currentPath) { - if (this.currentPath.shape.points.length) { - // halfway through a line? start a new one! - this.drawShape( new core.math.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. - * - * @param x {number} the X coordinate to move to - * @param y {number} the Y coordinate to move to - * @return {Graphics} - */ -Graphics.prototype.moveTo = function (x, y) { - this.drawShape(new core.math.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). - * - * @param x {number} the X coordinate to draw to - * @param y {number} the Y coordinate to draw to - * @return {Graphics} - */ -Graphics.prototype.lineTo = function (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 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} - */ -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; - - return this; -}; - -/** - * Calculate the points for a bezier curve and then draws it. - * - * @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} - */ -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; - - 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 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} - */ -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; - - return this; -}; - -/** - * The arc method creates an arc/curve (used to create circles, or parts of circles). - * - * @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. - * @return {Graphics} - */ -Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { - var startX = cx + Math.cos(startAngle) * radius; - var startY = cy + Math.sin(startAngle) * radius; - var points; - - if (this.currentPath) { - points = this.currentPath.shape.points; - - if (points.length === 0) { - points.push(startX, startY); - } - else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { - points.push(startX, startY); - } - } - else { - this.moveTo(startX, startY); - points = this.currentPath.shape.points; - } - - 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.abs(sweep)/ (Math.PI * 2)) * 40; - - if (sweep === 0) { - return this; - } - - 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; - - return this; -}; - -/** - * Specifies a simple one-color fill that subsequent calls to other Graphics methods - * (such as lineTo() or drawCircle()) use when drawing. - * - * @param color {number} the color of the fill - * @param alpha {number} the alpha of the fill - * @return {Graphics} - */ -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. - * - * @return {Graphics} - */ -Graphics.prototype.endFill = function () { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; - - return this; -}; - -/** - * - * @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} - */ -Graphics.prototype.drawRect = function ( x, y, width, height ) { - this.drawShape(new core.math.Rectangle(x,y, width, height)); - - return this; -}; - -/** - * - * @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 - */ -Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { - this.drawShape(new core.math.RoundedRectangle(x, y, width, height, radius)); - - return this; -}; - -/** - * Draws a circle. - * - * @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 radius {number} The radius of the circle - * @return {Graphics} - */ -Graphics.prototype.drawCircle = function (x, y, radius) { - this.drawShape(new core.math.Circle(x,y, radius)); - - return this; -}; - -/** - * Draws an ellipse. - * - * @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} - */ -Graphics.prototype.drawEllipse = function (x, y, width, height) { - this.drawShape(new core.math.Ellipse(x, y, width, height)); - - return this; -}; - -/** - * Draws a polygon using the given path. - * - * @param path {Array} The path data used to construct the polygon. - * @return {Graphics} - */ -Graphics.prototype.drawPolygon = function (path) { - if (!(path instanceof Array)) { - path = Array.prototype.slice.call(arguments); - } - - this.drawShape(new core.math.Polygon(path)); - - return this; -}; - -/** - * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. - * - * @return {Graphics} - */ -Graphics.prototype.clear = function () { - 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 resolution {number} The resolution of the texture being generated - * @param scaleMode {number} Should be one of the scaleMode consts - * @return {Texture} a texture of the graphics object - */ -Graphics.prototype.generateTexture = function (resolution, scaleMode) { - resolution = resolution || 1; - - var bounds = this.getBounds(); - - var canvasBuffer = new core.CanvasBuffer(bounds.width * resolution, bounds.height * resolution); - - var texture = core.Texture.fromCanvas(canvasBuffer.canvas, scaleMode); - texture.baseTexture.resolution = resolution; - - canvasBuffer.context.scale(resolution, resolution); - - canvasBuffer.context.translate(-bounds.x,-bounds.y); - - core.CanvasGraphics.renderGraphics(this, canvasBuffer.context); - - return texture; -}; - -/** - * Renders the object using the WebGL renderer - * - * @param renderer {WebGLRenderer} - */ -Graphics.prototype.renderWebGL = function (renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (!this.visible || 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; - - core.Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); - - return; - } - else { - renderer.spriteBatch.stop(); - renderer.blendModeManager.setBlendMode(this.blendMode); - - if (this._mask) { - renderer.maskManager.pushMask(this._mask, renderer); - } - - if (this._filters) { - renderer.filterManager.pushFilter(this._filterBlock); - } - - // check blend mode - if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { - renderer.spriteBatch.currentBlendMode = this.blendMode; - - var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; - - renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); - } - - // check if the webgl graphic needs to be updated - if (this.glDirty) { - this.dirty = true; - this.glDirty = false; - } - - core.WebGLGraphics.renderGraphics(this, renderer); - - // only render if it has children! - if (this.children.length) { - renderer.spriteBatch.start(); - - // simple render children! - for (var i = 0, j = this.children.length; i < j; ++i) { - this.children[i].renderWebGL(renderer); - } - - renderer.spriteBatch.stop(); - } - - if (this._filters) { - renderer.filterManager.popFilter(); - } - - if (this._mask) { - renderer.maskManager.popMask(this.mask, renderer); - } - - renderer.drawCount++; - - renderer.spriteBatch.start(); - } -}; - -/** - * Renders the object using the Canvas renderer - * - * @param renderer {CanvasRenderer} - * @private - */ -Graphics.prototype.renderCanvas = function (renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (!this.visible || 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 - this.updateCachedSpriteTexture(); - - this.cachedSpriteDirty = false; - this.dirty = false; - } - - this._cachedSprite.alpha = this.alpha; - - core.Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); - - return; - } - else { - var context = renderer.context; - var transform = this.worldTransform; - - if (this.blendMode !== renderer.currentBlendMode) { - renderer.currentBlendMode = this.blendMode; - context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; - } - - if (this._mask) { - renderer.maskManager.pushMask(this._mask, renderer); - } - - var resolution = renderer.resolution; - context.setTransform( - transform.a * resolution, - transform.b * resolution, - transform.c * resolution, - transform.d * resolution, - transform.tx * resolution, - transform.ty * resolution - ); - - core.CanvasGraphics.renderGraphics(this, context); - - for (var i = 0, j = this.children.length; i < j; ++i) { - this.children[i].renderCanvas(renderer); - } - - if (this._mask) { - renderer.maskManager.popMask(renderer); - } - } -}; - -/** - * Retrieves the bounds of the graphic shape as a rectangle object - * - * @return {Rectangle} the rectangular bounding area - */ -Graphics.prototype.getBounds = function (matrix) { - // return an empty object if the item is a mask! - if (this.isMask) { - return core.math.Rectangle.EMPTY; - } - - if (this.dirty) { - this.updateLocalBounds(); - - this.glDirty = 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; - - return this._bounds; -}; - -/** - * Update the bounds of the object - * - */ -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 === core.CONST.SHAPES.RECT || type === core.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 === core.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 === core.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; -}; - -/** - * Generates the cached sprite when the sprite has cacheAsBitmap = true - * - * @private - */ -Graphics.prototype._generateCachedSprite = function () { - var bounds = this.getLocalBounds(); - - if (!this._cachedSprite) { - var canvasBuffer = new core.CanvasBuffer(bounds.width, bounds.height); - var texture = core.Texture.fromCanvas(canvasBuffer.canvas); - - this._cachedSprite = new core.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.. - core.CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); - - this._cachedSprite.alpha = this.alpha; -}; - -/** - * Updates texture size based on canvas size - * - * @private - */ -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. - * - */ -Graphics.prototype.destroyCachedSprite = function () { - this._cachedSprite.texture.destroy(true); - - // let the gc collect the unused sprite - // TODO could be object pooled! - this._cachedSprite = null; -}; - -/** - * 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. - */ -Graphics.prototype.drawShape = function (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 === core.CONST.SHAPES.POLY) { - data.shape.closed = this.filling; - this.currentPath = data; - } - - this.dirty = true; - - return data; -}; diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/renderers/canvas/CanvasGraphics.js b/src/core/renderers/canvas/CanvasGraphics.js deleted file mode 100644 index ef7e765..0000000 --- a/src/core/renderers/canvas/CanvasGraphics.js +++ /dev/null @@ -1,318 +0,0 @@ -var CONST = require('../../const'); - -/** - * A set of functions used by the canvas renderer to draw the primitive graphics data. - * - * @namespace PIXI - */ -var CanvasGraphics = module.exports = {}; - -/* - * Renders a Graphics object to a canvas. - * - * @param graphics {Graphics} the actual graphics object to render - * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas - */ -CanvasGraphics.renderGraphics = function (graphics, context) { - var worldAlpha = graphics.worldAlpha; - - if (graphics.dirty) { - this.updateGraphicsTint(graphics); - graphics.dirty = false; - } - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - var fillColor = data._fillTint; - var lineColor = data._lineTint; - - context.lineWidth = data.lineWidth; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - if (shape.closed) { - context.lineTo(points[0], points[1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RECT) { - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fillRect(shape.x, shape.y, shape.width, shape.height); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.strokeRect(shape.x, shape.y, shape.width, shape.height); - } - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.ELIP) { - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RREC) { - var rx = shape.x; - var ry = shape.y; - var width = shape.width; - var height = shape.height; - var radius = shape.radius; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - } -}; - -/* - * Renders a graphics mask - * - * @private - * @param graphics {Graphics} the graphics which will be used as a mask - * @param context {CanvasRenderingContext2D} the context 2d method of the canvas - */ -CanvasGraphics.renderGraphicsMask = function (graphics, context) { - var len = graphics.graphicsData.length; - - if (len === 0) { - return; - } - - if (len > 1) { - len = 1; - window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); - } - - for (var i = 0; i < 1; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - } - else if (data.type === CONST.SHAPES.RECT) { - context.beginPath(); - context.rect(shape.x, shape.y, shape.width, shape.height); - context.closePath(); - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - } - else if (data.type === CONST.SHAPES.ELIP) { - - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - context.closePath(); - } - else if (data.type === CONST.SHAPES.RREC) { - - var pts = shape.points; - var rx = pts[0]; - var ry = pts[1]; - var width = pts[2]; - var height = pts[3]; - var radius = pts[4]; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - } - } -}; - -CanvasGraphics.updateGraphicsTint = function (graphics) { - if (graphics.tint === 0xFFFFFF) { - return; - } - - var tintR = (graphics.tint >> 16 & 0xFF) / 255; - var tintG = (graphics.tint >> 8 & 0xFF) / 255; - var tintB = (graphics.tint & 0xFF)/ 255; - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - - var fillColor = data.fillColor | 0; - var lineColor = data.lineColor | 0; - - /* - var colorR = (fillColor >> 16 & 0xFF) / 255; - var colorG = (fillColor >> 8 & 0xFF) / 255; - var colorB = (fillColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - - colorR = (lineColor >> 16 & 0xFF) / 255; - colorG = (lineColor >> 8 & 0xFF) / 255; - colorB = (lineColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - */ - - // super inline cos im an optimization NAZI :) - data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); - data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); - - } -}; - diff --git a/src/core/renderers/canvas/utils/CanvasGraphics.js b/src/core/renderers/canvas/utils/CanvasGraphics.js new file mode 100644 index 0000000..ef7e765 --- /dev/null +++ b/src/core/renderers/canvas/utils/CanvasGraphics.js @@ -0,0 +1,318 @@ +var CONST = require('../../const'); + +/** + * A set of functions used by the canvas renderer to draw the primitive graphics data. + * + * @namespace PIXI + */ +var CanvasGraphics = module.exports = {}; + +/* + * Renders a Graphics object to a canvas. + * + * @param graphics {Graphics} the actual graphics object to render + * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas + */ +CanvasGraphics.renderGraphics = function (graphics, context) { + var worldAlpha = graphics.worldAlpha; + + if (graphics.dirty) { + this.updateGraphicsTint(graphics); + graphics.dirty = false; + } + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + var fillColor = data._fillTint; + var lineColor = data._lineTint; + + context.lineWidth = data.lineWidth; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + if (shape.closed) { + context.lineTo(points[0], points[1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RECT) { + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fillRect(shape.x, shape.y, shape.width, shape.height); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.strokeRect(shape.x, shape.y, shape.width, shape.height); + } + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.ELIP) { + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RREC) { + var rx = shape.x; + var ry = shape.y; + var width = shape.width; + var height = shape.height; + var radius = shape.radius; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + } +}; + +/* + * Renders a graphics mask + * + * @private + * @param graphics {Graphics} the graphics which will be used as a mask + * @param context {CanvasRenderingContext2D} the context 2d method of the canvas + */ +CanvasGraphics.renderGraphicsMask = function (graphics, context) { + var len = graphics.graphicsData.length; + + if (len === 0) { + return; + } + + if (len > 1) { + len = 1; + window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); + } + + for (var i = 0; i < 1; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + } + else if (data.type === CONST.SHAPES.RECT) { + context.beginPath(); + context.rect(shape.x, shape.y, shape.width, shape.height); + context.closePath(); + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + } + else if (data.type === CONST.SHAPES.ELIP) { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + context.closePath(); + } + else if (data.type === CONST.SHAPES.RREC) { + + var pts = shape.points; + var rx = pts[0]; + var ry = pts[1]; + var width = pts[2]; + var height = pts[3]; + var radius = pts[4]; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + } + } +}; + +CanvasGraphics.updateGraphicsTint = function (graphics) { + if (graphics.tint === 0xFFFFFF) { + return; + } + + var tintR = (graphics.tint >> 16 & 0xFF) / 255; + var tintG = (graphics.tint >> 8 & 0xFF) / 255; + var tintB = (graphics.tint & 0xFF)/ 255; + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + + var fillColor = data.fillColor | 0; + var lineColor = data.lineColor | 0; + + /* + var colorR = (fillColor >> 16 & 0xFF) / 255; + var colorG = (fillColor >> 8 & 0xFF) / 255; + var colorB = (fillColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + + colorR = (lineColor >> 16 & 0xFF) / 255; + colorG = (lineColor >> 8 & 0xFF) / 255; + colorB = (lineColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + */ + + // super inline cos im an optimization NAZI :) + data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); + data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); + + } +}; + diff --git a/src/core/renderers/canvas/utils/CanvasMaskManager.js b/src/core/renderers/canvas/utils/CanvasMaskManager.js index 61ada5f..e04a733 100644 --- a/src/core/renderers/canvas/utils/CanvasMaskManager.js +++ b/src/core/renderers/canvas/utils/CanvasMaskManager.js @@ -1,4 +1,4 @@ -var CanvasGraphics = require('../CanvasGraphics'); +var CanvasGraphics = require('./CanvasGraphics'); /** * A set of functions used to handle masking. diff --git a/src/primitives/Graphics.js b/src/primitives/Graphics.js deleted file mode 100644 index 665eede..0000000 --- a/src/primitives/Graphics.js +++ /dev/null @@ -1,1026 +0,0 @@ -var core = require('../core'), - GraphicsData = require('./GraphicsData'); - -/** - * 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 - * @extends DisplayObjectContainer - * @namespace PIXI - */ -function Graphics() { - core.DisplayObjectContainer.call(this); - - this.renderable = true; - - /** - * 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 {object[]} - * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. - * - * @member {number} - * @default CONST.blendModes.NORMAL; - */ - this.blendMode = core.CONST.blendModes.NORMAL; - - /** - * Current path - * - * @member {object} - * @private - */ - this.currentPath = null; - - /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object[]} - * @private - */ - 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 core.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; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; -} - -// constructor -Graphics.prototype = Object.create(core.DisplayObjectContainer.prototype); -Graphics.prototype.constructor = Graphics; -module.exports = Graphics; - -Object.defineProperties(Graphics.prototype, { - /** - * 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. - * - * @member {boolean} - * @memberof Graphics# - * @default false - * @private - */ - cacheAsBitmap: { - get: function () { - return this._cacheAsBitmap; - }, - set: function (value) { - this._cacheAsBitmap = value; - - if (this._cacheAsBitmap) { - this._generateCachedSprite(); - } - else { - this.destroyCachedSprite(); - this.dirty = true; - } - } - } -}); - -/** - * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. - * - * @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} - */ -Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { - this.lineWidth = lineWidth || 0; - this.lineColor = color || 0; - this.lineAlpha = (arguments.length < 3) ? 1 : alpha; - - if (this.currentPath) { - if (this.currentPath.shape.points.length) { - // halfway through a line? start a new one! - this.drawShape( new core.math.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. - * - * @param x {number} the X coordinate to move to - * @param y {number} the Y coordinate to move to - * @return {Graphics} - */ -Graphics.prototype.moveTo = function (x, y) { - this.drawShape(new core.math.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). - * - * @param x {number} the X coordinate to draw to - * @param y {number} the Y coordinate to draw to - * @return {Graphics} - */ -Graphics.prototype.lineTo = function (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 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} - */ -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; - - return this; -}; - -/** - * Calculate the points for a bezier curve and then draws it. - * - * @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} - */ -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; - - 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 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} - */ -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; - - return this; -}; - -/** - * The arc method creates an arc/curve (used to create circles, or parts of circles). - * - * @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. - * @return {Graphics} - */ -Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { - var startX = cx + Math.cos(startAngle) * radius; - var startY = cy + Math.sin(startAngle) * radius; - var points; - - if (this.currentPath) { - points = this.currentPath.shape.points; - - if (points.length === 0) { - points.push(startX, startY); - } - else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { - points.push(startX, startY); - } - } - else { - this.moveTo(startX, startY); - points = this.currentPath.shape.points; - } - - 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.abs(sweep)/ (Math.PI * 2)) * 40; - - if (sweep === 0) { - return this; - } - - 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; - - return this; -}; - -/** - * Specifies a simple one-color fill that subsequent calls to other Graphics methods - * (such as lineTo() or drawCircle()) use when drawing. - * - * @param color {number} the color of the fill - * @param alpha {number} the alpha of the fill - * @return {Graphics} - */ -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. - * - * @return {Graphics} - */ -Graphics.prototype.endFill = function () { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; - - return this; -}; - -/** - * - * @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} - */ -Graphics.prototype.drawRect = function ( x, y, width, height ) { - this.drawShape(new core.math.Rectangle(x,y, width, height)); - - return this; -}; - -/** - * - * @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 - */ -Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { - this.drawShape(new core.math.RoundedRectangle(x, y, width, height, radius)); - - return this; -}; - -/** - * Draws a circle. - * - * @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 radius {number} The radius of the circle - * @return {Graphics} - */ -Graphics.prototype.drawCircle = function (x, y, radius) { - this.drawShape(new core.math.Circle(x,y, radius)); - - return this; -}; - -/** - * Draws an ellipse. - * - * @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} - */ -Graphics.prototype.drawEllipse = function (x, y, width, height) { - this.drawShape(new core.math.Ellipse(x, y, width, height)); - - return this; -}; - -/** - * Draws a polygon using the given path. - * - * @param path {Array} The path data used to construct the polygon. - * @return {Graphics} - */ -Graphics.prototype.drawPolygon = function (path) { - if (!(path instanceof Array)) { - path = Array.prototype.slice.call(arguments); - } - - this.drawShape(new core.math.Polygon(path)); - - return this; -}; - -/** - * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. - * - * @return {Graphics} - */ -Graphics.prototype.clear = function () { - 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 resolution {number} The resolution of the texture being generated - * @param scaleMode {number} Should be one of the scaleMode consts - * @return {Texture} a texture of the graphics object - */ -Graphics.prototype.generateTexture = function (resolution, scaleMode) { - resolution = resolution || 1; - - var bounds = this.getBounds(); - - var canvasBuffer = new core.CanvasBuffer(bounds.width * resolution, bounds.height * resolution); - - var texture = core.Texture.fromCanvas(canvasBuffer.canvas, scaleMode); - texture.baseTexture.resolution = resolution; - - canvasBuffer.context.scale(resolution, resolution); - - canvasBuffer.context.translate(-bounds.x,-bounds.y); - - core.CanvasGraphics.renderGraphics(this, canvasBuffer.context); - - return texture; -}; - -/** - * Renders the object using the WebGL renderer - * - * @param renderer {WebGLRenderer} - */ -Graphics.prototype.renderWebGL = function (renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (!this.visible || 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; - - core.Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); - - return; - } - else { - renderer.spriteBatch.stop(); - renderer.blendModeManager.setBlendMode(this.blendMode); - - if (this._mask) { - renderer.maskManager.pushMask(this._mask, renderer); - } - - if (this._filters) { - renderer.filterManager.pushFilter(this._filterBlock); - } - - // check blend mode - if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { - renderer.spriteBatch.currentBlendMode = this.blendMode; - - var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; - - renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); - } - - // check if the webgl graphic needs to be updated - if (this.glDirty) { - this.dirty = true; - this.glDirty = false; - } - - core.WebGLGraphics.renderGraphics(this, renderer); - - // only render if it has children! - if (this.children.length) { - renderer.spriteBatch.start(); - - // simple render children! - for (var i = 0, j = this.children.length; i < j; ++i) { - this.children[i].renderWebGL(renderer); - } - - renderer.spriteBatch.stop(); - } - - if (this._filters) { - renderer.filterManager.popFilter(); - } - - if (this._mask) { - renderer.maskManager.popMask(this.mask, renderer); - } - - renderer.drawCount++; - - renderer.spriteBatch.start(); - } -}; - -/** - * Renders the object using the Canvas renderer - * - * @param renderer {CanvasRenderer} - * @private - */ -Graphics.prototype.renderCanvas = function (renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (!this.visible || 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 - this.updateCachedSpriteTexture(); - - this.cachedSpriteDirty = false; - this.dirty = false; - } - - this._cachedSprite.alpha = this.alpha; - - core.Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); - - return; - } - else { - var context = renderer.context; - var transform = this.worldTransform; - - if (this.blendMode !== renderer.currentBlendMode) { - renderer.currentBlendMode = this.blendMode; - context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; - } - - if (this._mask) { - renderer.maskManager.pushMask(this._mask, renderer); - } - - var resolution = renderer.resolution; - context.setTransform( - transform.a * resolution, - transform.b * resolution, - transform.c * resolution, - transform.d * resolution, - transform.tx * resolution, - transform.ty * resolution - ); - - core.CanvasGraphics.renderGraphics(this, context); - - for (var i = 0, j = this.children.length; i < j; ++i) { - this.children[i].renderCanvas(renderer); - } - - if (this._mask) { - renderer.maskManager.popMask(renderer); - } - } -}; - -/** - * Retrieves the bounds of the graphic shape as a rectangle object - * - * @return {Rectangle} the rectangular bounding area - */ -Graphics.prototype.getBounds = function (matrix) { - // return an empty object if the item is a mask! - if (this.isMask) { - return core.math.Rectangle.EMPTY; - } - - if (this.dirty) { - this.updateLocalBounds(); - - this.glDirty = 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; - - return this._bounds; -}; - -/** - * Update the bounds of the object - * - */ -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 === core.CONST.SHAPES.RECT || type === core.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 === core.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 === core.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; -}; - -/** - * Generates the cached sprite when the sprite has cacheAsBitmap = true - * - * @private - */ -Graphics.prototype._generateCachedSprite = function () { - var bounds = this.getLocalBounds(); - - if (!this._cachedSprite) { - var canvasBuffer = new core.CanvasBuffer(bounds.width, bounds.height); - var texture = core.Texture.fromCanvas(canvasBuffer.canvas); - - this._cachedSprite = new core.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.. - core.CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); - - this._cachedSprite.alpha = this.alpha; -}; - -/** - * Updates texture size based on canvas size - * - * @private - */ -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. - * - */ -Graphics.prototype.destroyCachedSprite = function () { - this._cachedSprite.texture.destroy(true); - - // let the gc collect the unused sprite - // TODO could be object pooled! - this._cachedSprite = null; -}; - -/** - * 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. - */ -Graphics.prototype.drawShape = function (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 === core.CONST.SHAPES.POLY) { - data.shape.closed = this.filling; - this.currentPath = data; - } - - this.dirty = true; - - return data; -}; diff --git a/src/primitives/GraphicsData.js b/src/primitives/GraphicsData.js deleted file mode 100644 index 9377ef7..0000000 --- a/src/primitives/GraphicsData.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * A GraphicsData object. - * - * @class - * @namespace PIXI - */ -function GraphicsData(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; -} - -GraphicsData.prototype.constructor = GraphicsData; -module.exports = GraphicsData; diff --git a/src/core/index.js b/src/core/index.js index fb28d11..e575eb8 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,7 +29,7 @@ // renderers - canvas CanvasRenderer: require('./renderers/canvas/CanvasRenderer'), - CanvasGraphics: require('./renderers/canvas/CanvasGraphics'), + CanvasGraphics: require('./renderers/canvas/utils/CanvasGraphics'), CanvasBuffer: require('./renderers/canvas/utils/CanvasBuffer'), // renderers - webgl diff --git a/src/core/primitives/Graphics.js b/src/core/primitives/Graphics.js new file mode 100644 index 0000000..b4966a5 --- /dev/null +++ b/src/core/primitives/Graphics.js @@ -0,0 +1,1033 @@ +var DisplayObjectContainer = require('../display/DisplayObjectContainer'), + Sprite = require('../display/Sprite'), + Texture = require('../textures/Texture'), + CanvasBuffer = require('../renderers/canvas/utils/CanvasBuffer'), + CanvasGraphics = require('../renderers/canvas/utils/CanvasGraphics'), + WebGLGraphics = require('../renderers/webgl/utils/WebGLGraphics'), + GraphicsData = require('./GraphicsData'), + math = require('../math'), + CONST = require('../const'); + +/** + * 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 + * @extends DisplayObjectContainer + * @namespace PIXI + */ +function Graphics() { + DisplayObjectContainer.call(this); + + this.renderable = true; + + /** + * 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 {object[]} + * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. + * + * @member {number} + * @default CONST.blendModes.NORMAL; + */ + this.blendMode = CONST.blendModes.NORMAL; + + /** + * Current path + * + * @member {object} + * @private + */ + this.currentPath = null; + + /** + * Array containing some WebGL-related properties used by the WebGL renderer. + * + * @member {object[]} + * @private + */ + 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; + + /** + * Used to detect if the cached sprite object needs to be updated. + * + * @member {boolean} + * @private + */ + this.cachedSpriteDirty = false; +} + +// constructor +Graphics.prototype = Object.create(DisplayObjectContainer.prototype); +Graphics.prototype.constructor = Graphics; +module.exports = Graphics; + +Object.defineProperties(Graphics.prototype, { + /** + * 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. + * + * @member {boolean} + * @memberof Graphics# + * @default false + * @private + */ + cacheAsBitmap: { + get: function () { + return this._cacheAsBitmap; + }, + set: function (value) { + this._cacheAsBitmap = value; + + if (this._cacheAsBitmap) { + this._generateCachedSprite(); + } + else { + this.destroyCachedSprite(); + this.dirty = true; + } + } + } +}); + +/** + * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. + * + * @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} + */ +Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { + this.lineWidth = lineWidth || 0; + this.lineColor = color || 0; + this.lineAlpha = (arguments.length < 3) ? 1 : alpha; + + if (this.currentPath) { + if (this.currentPath.shape.points.length) { + // halfway through a line? start a new one! + this.drawShape( new math.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. + * + * @param x {number} the X coordinate to move to + * @param y {number} the Y coordinate to move to + * @return {Graphics} + */ +Graphics.prototype.moveTo = function (x, y) { + this.drawShape(new math.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). + * + * @param x {number} the X coordinate to draw to + * @param y {number} the Y coordinate to draw to + * @return {Graphics} + */ +Graphics.prototype.lineTo = function (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 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} + */ +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; + + return this; +}; + +/** + * Calculate the points for a bezier curve and then draws it. + * + * @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} + */ +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; + + 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 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} + */ +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; + + return this; +}; + +/** + * The arc method creates an arc/curve (used to create circles, or parts of circles). + * + * @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. + * @return {Graphics} + */ +Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { + var startX = cx + Math.cos(startAngle) * radius; + var startY = cy + Math.sin(startAngle) * radius; + var points; + + if (this.currentPath) { + points = this.currentPath.shape.points; + + if (points.length === 0) { + points.push(startX, startY); + } + else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { + points.push(startX, startY); + } + } + else { + this.moveTo(startX, startY); + points = this.currentPath.shape.points; + } + + 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.abs(sweep)/ (Math.PI * 2)) * 40; + + if (sweep === 0) { + return this; + } + + 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; + + return this; +}; + +/** + * Specifies a simple one-color fill that subsequent calls to other Graphics methods + * (such as lineTo() or drawCircle()) use when drawing. + * + * @param color {number} the color of the fill + * @param alpha {number} the alpha of the fill + * @return {Graphics} + */ +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. + * + * @return {Graphics} + */ +Graphics.prototype.endFill = function () { + this.filling = false; + this.fillColor = null; + this.fillAlpha = 1; + + return this; +}; + +/** + * + * @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} + */ +Graphics.prototype.drawRect = function ( x, y, width, height ) { + this.drawShape(new math.Rectangle(x,y, width, height)); + + return this; +}; + +/** + * + * @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 + */ +Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { + this.drawShape(new math.RoundedRectangle(x, y, width, height, radius)); + + return this; +}; + +/** + * Draws a circle. + * + * @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 radius {number} The radius of the circle + * @return {Graphics} + */ +Graphics.prototype.drawCircle = function (x, y, radius) { + this.drawShape(new math.Circle(x,y, radius)); + + return this; +}; + +/** + * Draws an ellipse. + * + * @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} + */ +Graphics.prototype.drawEllipse = function (x, y, width, height) { + this.drawShape(new math.Ellipse(x, y, width, height)); + + return this; +}; + +/** + * Draws a polygon using the given path. + * + * @param path {Array} The path data used to construct the polygon. + * @return {Graphics} + */ +Graphics.prototype.drawPolygon = function (path) { + if (!(path instanceof Array)) { + path = Array.prototype.slice.call(arguments); + } + + this.drawShape(new math.Polygon(path)); + + return this; +}; + +/** + * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. + * + * @return {Graphics} + */ +Graphics.prototype.clear = function () { + 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 resolution {number} The resolution of the texture being generated + * @param scaleMode {number} Should be one of the scaleMode consts + * @return {Texture} a texture of the graphics object + */ +Graphics.prototype.generateTexture = function (resolution, scaleMode) { + resolution = resolution || 1; + + var bounds = this.getBounds(); + + 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 renderer {WebGLRenderer} + */ +Graphics.prototype.renderWebGL = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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; + + Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); + + return; + } + else { + renderer.spriteBatch.stop(); + renderer.blendModeManager.setBlendMode(this.blendMode); + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + if (this._filters) { + renderer.filterManager.pushFilter(this._filterBlock); + } + + // check blend mode + if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { + renderer.spriteBatch.currentBlendMode = this.blendMode; + + var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; + + renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); + } + + // check if the webgl graphic needs to be updated + if (this.glDirty) { + this.dirty = true; + this.glDirty = false; + } + + WebGLGraphics.renderGraphics(this, renderer); + + // only render if it has children! + if (this.children.length) { + renderer.spriteBatch.start(); + + // simple render children! + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderWebGL(renderer); + } + + renderer.spriteBatch.stop(); + } + + if (this._filters) { + renderer.filterManager.popFilter(); + } + + if (this._mask) { + renderer.maskManager.popMask(this.mask, renderer); + } + + renderer.drawCount++; + + renderer.spriteBatch.start(); + } +}; + +/** + * Renders the object using the Canvas renderer + * + * @param renderer {CanvasRenderer} + * @private + */ +Graphics.prototype.renderCanvas = function (renderer) { + // if the sprite is not visible or the alpha is 0 then no need to render this element + if (!this.visible || 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 + this.updateCachedSpriteTexture(); + + this.cachedSpriteDirty = false; + this.dirty = false; + } + + this._cachedSprite.alpha = this.alpha; + + Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); + + return; + } + else { + var context = renderer.context; + var transform = this.worldTransform; + + if (this.blendMode !== renderer.currentBlendMode) { + renderer.currentBlendMode = this.blendMode; + context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; + } + + if (this._mask) { + renderer.maskManager.pushMask(this._mask, renderer); + } + + 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); + + for (var i = 0, j = this.children.length; i < j; ++i) { + this.children[i].renderCanvas(renderer); + } + + if (this._mask) { + renderer.maskManager.popMask(renderer); + } + } +}; + +/** + * Retrieves the bounds of the graphic shape as a rectangle object + * + * @return {Rectangle} the rectangular bounding area + */ +Graphics.prototype.getBounds = function (matrix) { + // return an empty object if the item is a mask! + if (this.isMask) { + return math.Rectangle.EMPTY; + } + + if (this.dirty) { + this.updateLocalBounds(); + + this.glDirty = 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; + + return this._bounds; +}; + +/** + * Update the bounds of the object + * + */ +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 === 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; +}; + +/** + * Generates the cached sprite when the sprite has cacheAsBitmap = true + * + * @private + */ +Graphics.prototype._generateCachedSprite = function () { + var bounds = this.getLocalBounds(); + + if (!this._cachedSprite) { + var canvasBuffer = new CanvasBuffer(bounds.width, bounds.height); + var texture = Texture.fromCanvas(canvasBuffer.canvas); + + this._cachedSprite = new 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.. + CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); + + this._cachedSprite.alpha = this.alpha; +}; + +/** + * Updates texture size based on canvas size + * + * @private + */ +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. + * + */ +Graphics.prototype.destroyCachedSprite = function () { + this._cachedSprite.texture.destroy(true); + + // let the gc collect the unused sprite + // TODO could be object pooled! + this._cachedSprite = null; +}; + +/** + * 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. + */ +Graphics.prototype.drawShape = function (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 = this.filling; + this.currentPath = data; + } + + this.dirty = true; + + return data; +}; diff --git a/src/core/primitives/GraphicsData.js b/src/core/primitives/GraphicsData.js new file mode 100644 index 0000000..9377ef7 --- /dev/null +++ b/src/core/primitives/GraphicsData.js @@ -0,0 +1,23 @@ +/** + * A GraphicsData object. + * + * @class + * @namespace PIXI + */ +function GraphicsData(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; +} + +GraphicsData.prototype.constructor = GraphicsData; +module.exports = GraphicsData; diff --git a/src/core/renderers/canvas/CanvasGraphics.js b/src/core/renderers/canvas/CanvasGraphics.js deleted file mode 100644 index ef7e765..0000000 --- a/src/core/renderers/canvas/CanvasGraphics.js +++ /dev/null @@ -1,318 +0,0 @@ -var CONST = require('../../const'); - -/** - * A set of functions used by the canvas renderer to draw the primitive graphics data. - * - * @namespace PIXI - */ -var CanvasGraphics = module.exports = {}; - -/* - * Renders a Graphics object to a canvas. - * - * @param graphics {Graphics} the actual graphics object to render - * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas - */ -CanvasGraphics.renderGraphics = function (graphics, context) { - var worldAlpha = graphics.worldAlpha; - - if (graphics.dirty) { - this.updateGraphicsTint(graphics); - graphics.dirty = false; - } - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - var fillColor = data._fillTint; - var lineColor = data._lineTint; - - context.lineWidth = data.lineWidth; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - if (shape.closed) { - context.lineTo(points[0], points[1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RECT) { - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fillRect(shape.x, shape.y, shape.width, shape.height); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.strokeRect(shape.x, shape.y, shape.width, shape.height); - } - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.ELIP) { - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - - context.closePath(); - - if (data.fill) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - else if (data.type === CONST.SHAPES.RREC) { - var rx = shape.x; - var ry = shape.y; - var width = shape.width; - var height = shape.height; - var radius = shape.radius; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - - if (data.fillColor || data.fillColor === 0) { - context.globalAlpha = data.fillAlpha * worldAlpha; - context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); - context.fill(); - - } - if (data.lineWidth) { - context.globalAlpha = data.lineAlpha * worldAlpha; - context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); - context.stroke(); - } - } - } -}; - -/* - * Renders a graphics mask - * - * @private - * @param graphics {Graphics} the graphics which will be used as a mask - * @param context {CanvasRenderingContext2D} the context 2d method of the canvas - */ -CanvasGraphics.renderGraphicsMask = function (graphics, context) { - var len = graphics.graphicsData.length; - - if (len === 0) { - return; - } - - if (len > 1) { - len = 1; - window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); - } - - for (var i = 0; i < 1; i++) { - var data = graphics.graphicsData[i]; - var shape = data.shape; - - if (data.type === CONST.SHAPES.POLY) { - context.beginPath(); - - var points = shape.points; - - context.moveTo(points[0], points[1]); - - for (var j=1; j < points.length/2; j++) { - context.lineTo(points[j * 2], points[j * 2 + 1]); - } - - // if the first and last point are the same close the path - much neater :) - if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { - context.closePath(); - } - - } - else if (data.type === CONST.SHAPES.RECT) { - context.beginPath(); - context.rect(shape.x, shape.y, shape.width, shape.height); - context.closePath(); - } - else if (data.type === CONST.SHAPES.CIRC) { - // TODO - need to be Undefined! - context.beginPath(); - context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); - context.closePath(); - } - else if (data.type === CONST.SHAPES.ELIP) { - - // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - - var w = shape.width * 2; - var h = shape.height * 2; - - var x = shape.x - w/2; - var y = shape.y - h/2; - - context.beginPath(); - - var kappa = 0.5522848, - ox = (w / 2) * kappa, // control point offset horizontal - oy = (h / 2) * kappa, // control point offset vertical - xe = x + w, // x-end - ye = y + h, // y-end - xm = x + w / 2, // x-middle - ym = y + h / 2; // y-middle - - context.moveTo(x, ym); - context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - context.closePath(); - } - else if (data.type === CONST.SHAPES.RREC) { - - var pts = shape.points; - var rx = pts[0]; - var ry = pts[1]; - var width = pts[2]; - var height = pts[3]; - var radius = pts[4]; - - var maxRadius = Math.min(width, height) / 2 | 0; - radius = radius > maxRadius ? maxRadius : radius; - - context.beginPath(); - context.moveTo(rx, ry + radius); - context.lineTo(rx, ry + height - radius); - context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); - context.lineTo(rx + width - radius, ry + height); - context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); - context.lineTo(rx + width, ry + radius); - context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); - context.lineTo(rx + radius, ry); - context.quadraticCurveTo(rx, ry, rx, ry + radius); - context.closePath(); - } - } -}; - -CanvasGraphics.updateGraphicsTint = function (graphics) { - if (graphics.tint === 0xFFFFFF) { - return; - } - - var tintR = (graphics.tint >> 16 & 0xFF) / 255; - var tintG = (graphics.tint >> 8 & 0xFF) / 255; - var tintB = (graphics.tint & 0xFF)/ 255; - - for (var i = 0; i < graphics.graphicsData.length; i++) { - var data = graphics.graphicsData[i]; - - var fillColor = data.fillColor | 0; - var lineColor = data.lineColor | 0; - - /* - var colorR = (fillColor >> 16 & 0xFF) / 255; - var colorG = (fillColor >> 8 & 0xFF) / 255; - var colorB = (fillColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - - colorR = (lineColor >> 16 & 0xFF) / 255; - colorG = (lineColor >> 8 & 0xFF) / 255; - colorB = (lineColor & 0xFF) / 255; - - colorR *= tintR; - colorG *= tintG; - colorB *= tintB; - - lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); - */ - - // super inline cos im an optimization NAZI :) - data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); - data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); - - } -}; - diff --git a/src/core/renderers/canvas/utils/CanvasGraphics.js b/src/core/renderers/canvas/utils/CanvasGraphics.js new file mode 100644 index 0000000..ef7e765 --- /dev/null +++ b/src/core/renderers/canvas/utils/CanvasGraphics.js @@ -0,0 +1,318 @@ +var CONST = require('../../const'); + +/** + * A set of functions used by the canvas renderer to draw the primitive graphics data. + * + * @namespace PIXI + */ +var CanvasGraphics = module.exports = {}; + +/* + * Renders a Graphics object to a canvas. + * + * @param graphics {Graphics} the actual graphics object to render + * @param context {CanvasRenderingContext2D} the 2d drawing method of the canvas + */ +CanvasGraphics.renderGraphics = function (graphics, context) { + var worldAlpha = graphics.worldAlpha; + + if (graphics.dirty) { + this.updateGraphicsTint(graphics); + graphics.dirty = false; + } + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + var fillColor = data._fillTint; + var lineColor = data._lineTint; + + context.lineWidth = data.lineWidth; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + if (shape.closed) { + context.lineTo(points[0], points[1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RECT) { + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fillRect(shape.x, shape.y, shape.width, shape.height); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.strokeRect(shape.x, shape.y, shape.width, shape.height); + } + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.ELIP) { + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + context.closePath(); + + if (data.fill) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + else if (data.type === CONST.SHAPES.RREC) { + var rx = shape.x; + var ry = shape.y; + var width = shape.width; + var height = shape.height; + var radius = shape.radius; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + + if (data.fillColor || data.fillColor === 0) { + context.globalAlpha = data.fillAlpha * worldAlpha; + context.fillStyle = '#' + ('00000' + ( fillColor | 0).toString(16)).substr(-6); + context.fill(); + + } + if (data.lineWidth) { + context.globalAlpha = data.lineAlpha * worldAlpha; + context.strokeStyle = '#' + ('00000' + ( lineColor | 0).toString(16)).substr(-6); + context.stroke(); + } + } + } +}; + +/* + * Renders a graphics mask + * + * @private + * @param graphics {Graphics} the graphics which will be used as a mask + * @param context {CanvasRenderingContext2D} the context 2d method of the canvas + */ +CanvasGraphics.renderGraphicsMask = function (graphics, context) { + var len = graphics.graphicsData.length; + + if (len === 0) { + return; + } + + if (len > 1) { + len = 1; + window.console.log('Pixi.js warning: masks in canvas can only mask using the first path in the graphics object'); + } + + for (var i = 0; i < 1; i++) { + var data = graphics.graphicsData[i]; + var shape = data.shape; + + if (data.type === CONST.SHAPES.POLY) { + context.beginPath(); + + var points = shape.points; + + context.moveTo(points[0], points[1]); + + for (var j=1; j < points.length/2; j++) { + context.lineTo(points[j * 2], points[j * 2 + 1]); + } + + // if the first and last point are the same close the path - much neater :) + if (points[0] === points[points.length-2] && points[1] === points[points.length-1]) { + context.closePath(); + } + + } + else if (data.type === CONST.SHAPES.RECT) { + context.beginPath(); + context.rect(shape.x, shape.y, shape.width, shape.height); + context.closePath(); + } + else if (data.type === CONST.SHAPES.CIRC) { + // TODO - need to be Undefined! + context.beginPath(); + context.arc(shape.x, shape.y, shape.radius,0,2*Math.PI); + context.closePath(); + } + else if (data.type === CONST.SHAPES.ELIP) { + + // ellipse code taken from: http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + + var w = shape.width * 2; + var h = shape.height * 2; + + var x = shape.x - w/2; + var y = shape.y - h/2; + + context.beginPath(); + + var kappa = 0.5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + context.moveTo(x, ym); + context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + context.closePath(); + } + else if (data.type === CONST.SHAPES.RREC) { + + var pts = shape.points; + var rx = pts[0]; + var ry = pts[1]; + var width = pts[2]; + var height = pts[3]; + var radius = pts[4]; + + var maxRadius = Math.min(width, height) / 2 | 0; + radius = radius > maxRadius ? maxRadius : radius; + + context.beginPath(); + context.moveTo(rx, ry + radius); + context.lineTo(rx, ry + height - radius); + context.quadraticCurveTo(rx, ry + height, rx + radius, ry + height); + context.lineTo(rx + width - radius, ry + height); + context.quadraticCurveTo(rx + width, ry + height, rx + width, ry + height - radius); + context.lineTo(rx + width, ry + radius); + context.quadraticCurveTo(rx + width, ry, rx + width - radius, ry); + context.lineTo(rx + radius, ry); + context.quadraticCurveTo(rx, ry, rx, ry + radius); + context.closePath(); + } + } +}; + +CanvasGraphics.updateGraphicsTint = function (graphics) { + if (graphics.tint === 0xFFFFFF) { + return; + } + + var tintR = (graphics.tint >> 16 & 0xFF) / 255; + var tintG = (graphics.tint >> 8 & 0xFF) / 255; + var tintB = (graphics.tint & 0xFF)/ 255; + + for (var i = 0; i < graphics.graphicsData.length; i++) { + var data = graphics.graphicsData[i]; + + var fillColor = data.fillColor | 0; + var lineColor = data.lineColor | 0; + + /* + var colorR = (fillColor >> 16 & 0xFF) / 255; + var colorG = (fillColor >> 8 & 0xFF) / 255; + var colorB = (fillColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + fillColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + + colorR = (lineColor >> 16 & 0xFF) / 255; + colorG = (lineColor >> 8 & 0xFF) / 255; + colorB = (lineColor & 0xFF) / 255; + + colorR *= tintR; + colorG *= tintG; + colorB *= tintB; + + lineColor = ((colorR*255 << 16) + (colorG*255 << 8) + colorB*255); + */ + + // super inline cos im an optimization NAZI :) + data._fillTint = (((fillColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((fillColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (fillColor & 0xFF) / 255 * tintB*255); + data._lineTint = (((lineColor >> 16 & 0xFF) / 255 * tintR*255 << 16) + ((lineColor >> 8 & 0xFF) / 255 * tintG*255 << 8) + (lineColor & 0xFF) / 255 * tintB*255); + + } +}; + diff --git a/src/core/renderers/canvas/utils/CanvasMaskManager.js b/src/core/renderers/canvas/utils/CanvasMaskManager.js index 61ada5f..e04a733 100644 --- a/src/core/renderers/canvas/utils/CanvasMaskManager.js +++ b/src/core/renderers/canvas/utils/CanvasMaskManager.js @@ -1,4 +1,4 @@ -var CanvasGraphics = require('../CanvasGraphics'); +var CanvasGraphics = require('./CanvasGraphics'); /** * A set of functions used to handle masking. diff --git a/src/primitives/Graphics.js b/src/primitives/Graphics.js deleted file mode 100644 index 665eede..0000000 --- a/src/primitives/Graphics.js +++ /dev/null @@ -1,1026 +0,0 @@ -var core = require('../core'), - GraphicsData = require('./GraphicsData'); - -/** - * 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 - * @extends DisplayObjectContainer - * @namespace PIXI - */ -function Graphics() { - core.DisplayObjectContainer.call(this); - - this.renderable = true; - - /** - * 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 {object[]} - * @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 blend mode to be applied to the graphic shape. Apply a value of blendModes.NORMAL to reset the blend mode. - * - * @member {number} - * @default CONST.blendModes.NORMAL; - */ - this.blendMode = core.CONST.blendModes.NORMAL; - - /** - * Current path - * - * @member {object} - * @private - */ - this.currentPath = null; - - /** - * Array containing some WebGL-related properties used by the WebGL renderer. - * - * @member {object[]} - * @private - */ - 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 core.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; - - /** - * Used to detect if the cached sprite object needs to be updated. - * - * @member {boolean} - * @private - */ - this.cachedSpriteDirty = false; -} - -// constructor -Graphics.prototype = Object.create(core.DisplayObjectContainer.prototype); -Graphics.prototype.constructor = Graphics; -module.exports = Graphics; - -Object.defineProperties(Graphics.prototype, { - /** - * 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. - * - * @member {boolean} - * @memberof Graphics# - * @default false - * @private - */ - cacheAsBitmap: { - get: function () { - return this._cacheAsBitmap; - }, - set: function (value) { - this._cacheAsBitmap = value; - - if (this._cacheAsBitmap) { - this._generateCachedSprite(); - } - else { - this.destroyCachedSprite(); - this.dirty = true; - } - } - } -}); - -/** - * Specifies the line style used for subsequent calls to Graphics methods such as the lineTo() method or the drawCircle() method. - * - * @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} - */ -Graphics.prototype.lineStyle = function (lineWidth, color, alpha) { - this.lineWidth = lineWidth || 0; - this.lineColor = color || 0; - this.lineAlpha = (arguments.length < 3) ? 1 : alpha; - - if (this.currentPath) { - if (this.currentPath.shape.points.length) { - // halfway through a line? start a new one! - this.drawShape( new core.math.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. - * - * @param x {number} the X coordinate to move to - * @param y {number} the Y coordinate to move to - * @return {Graphics} - */ -Graphics.prototype.moveTo = function (x, y) { - this.drawShape(new core.math.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). - * - * @param x {number} the X coordinate to draw to - * @param y {number} the Y coordinate to draw to - * @return {Graphics} - */ -Graphics.prototype.lineTo = function (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 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} - */ -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; - - return this; -}; - -/** - * Calculate the points for a bezier curve and then draws it. - * - * @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} - */ -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; - - 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 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} - */ -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; - - return this; -}; - -/** - * The arc method creates an arc/curve (used to create circles, or parts of circles). - * - * @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. - * @return {Graphics} - */ -Graphics.prototype.arc = function (cx, cy, radius, startAngle, endAngle, anticlockwise) { - var startX = cx + Math.cos(startAngle) * radius; - var startY = cy + Math.sin(startAngle) * radius; - var points; - - if (this.currentPath) { - points = this.currentPath.shape.points; - - if (points.length === 0) { - points.push(startX, startY); - } - else if (points[points.length-2] !== startX || points[points.length-1] !== startY) { - points.push(startX, startY); - } - } - else { - this.moveTo(startX, startY); - points = this.currentPath.shape.points; - } - - 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.abs(sweep)/ (Math.PI * 2)) * 40; - - if (sweep === 0) { - return this; - } - - 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; - - return this; -}; - -/** - * Specifies a simple one-color fill that subsequent calls to other Graphics methods - * (such as lineTo() or drawCircle()) use when drawing. - * - * @param color {number} the color of the fill - * @param alpha {number} the alpha of the fill - * @return {Graphics} - */ -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. - * - * @return {Graphics} - */ -Graphics.prototype.endFill = function () { - this.filling = false; - this.fillColor = null; - this.fillAlpha = 1; - - return this; -}; - -/** - * - * @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} - */ -Graphics.prototype.drawRect = function ( x, y, width, height ) { - this.drawShape(new core.math.Rectangle(x,y, width, height)); - - return this; -}; - -/** - * - * @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 - */ -Graphics.prototype.drawRoundedRect = function ( x, y, width, height, radius ) { - this.drawShape(new core.math.RoundedRectangle(x, y, width, height, radius)); - - return this; -}; - -/** - * Draws a circle. - * - * @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 radius {number} The radius of the circle - * @return {Graphics} - */ -Graphics.prototype.drawCircle = function (x, y, radius) { - this.drawShape(new core.math.Circle(x,y, radius)); - - return this; -}; - -/** - * Draws an ellipse. - * - * @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} - */ -Graphics.prototype.drawEllipse = function (x, y, width, height) { - this.drawShape(new core.math.Ellipse(x, y, width, height)); - - return this; -}; - -/** - * Draws a polygon using the given path. - * - * @param path {Array} The path data used to construct the polygon. - * @return {Graphics} - */ -Graphics.prototype.drawPolygon = function (path) { - if (!(path instanceof Array)) { - path = Array.prototype.slice.call(arguments); - } - - this.drawShape(new core.math.Polygon(path)); - - return this; -}; - -/** - * Clears the graphics that were drawn to this Graphics object, and resets fill and line style settings. - * - * @return {Graphics} - */ -Graphics.prototype.clear = function () { - 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 resolution {number} The resolution of the texture being generated - * @param scaleMode {number} Should be one of the scaleMode consts - * @return {Texture} a texture of the graphics object - */ -Graphics.prototype.generateTexture = function (resolution, scaleMode) { - resolution = resolution || 1; - - var bounds = this.getBounds(); - - var canvasBuffer = new core.CanvasBuffer(bounds.width * resolution, bounds.height * resolution); - - var texture = core.Texture.fromCanvas(canvasBuffer.canvas, scaleMode); - texture.baseTexture.resolution = resolution; - - canvasBuffer.context.scale(resolution, resolution); - - canvasBuffer.context.translate(-bounds.x,-bounds.y); - - core.CanvasGraphics.renderGraphics(this, canvasBuffer.context); - - return texture; -}; - -/** - * Renders the object using the WebGL renderer - * - * @param renderer {WebGLRenderer} - */ -Graphics.prototype.renderWebGL = function (renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (!this.visible || 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; - - core.Sprite.prototype.renderWebGL.call(this._cachedSprite, renderer); - - return; - } - else { - renderer.spriteBatch.stop(); - renderer.blendModeManager.setBlendMode(this.blendMode); - - if (this._mask) { - renderer.maskManager.pushMask(this._mask, renderer); - } - - if (this._filters) { - renderer.filterManager.pushFilter(this._filterBlock); - } - - // check blend mode - if (this.blendMode !== renderer.spriteBatch.currentBlendMode) { - renderer.spriteBatch.currentBlendMode = this.blendMode; - - var blendModeWebGL = blendModesWebGL[renderer.spriteBatch.currentBlendMode]; - - renderer.spriteBatch.gl.blendFunc(blendModeWebGL[0], blendModeWebGL[1]); - } - - // check if the webgl graphic needs to be updated - if (this.glDirty) { - this.dirty = true; - this.glDirty = false; - } - - core.WebGLGraphics.renderGraphics(this, renderer); - - // only render if it has children! - if (this.children.length) { - renderer.spriteBatch.start(); - - // simple render children! - for (var i = 0, j = this.children.length; i < j; ++i) { - this.children[i].renderWebGL(renderer); - } - - renderer.spriteBatch.stop(); - } - - if (this._filters) { - renderer.filterManager.popFilter(); - } - - if (this._mask) { - renderer.maskManager.popMask(this.mask, renderer); - } - - renderer.drawCount++; - - renderer.spriteBatch.start(); - } -}; - -/** - * Renders the object using the Canvas renderer - * - * @param renderer {CanvasRenderer} - * @private - */ -Graphics.prototype.renderCanvas = function (renderer) { - // if the sprite is not visible or the alpha is 0 then no need to render this element - if (!this.visible || 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 - this.updateCachedSpriteTexture(); - - this.cachedSpriteDirty = false; - this.dirty = false; - } - - this._cachedSprite.alpha = this.alpha; - - core.Sprite.prototype.renderCanvas.call(this._cachedSprite, renderer); - - return; - } - else { - var context = renderer.context; - var transform = this.worldTransform; - - if (this.blendMode !== renderer.currentBlendMode) { - renderer.currentBlendMode = this.blendMode; - context.globalCompositeOperation = blendModesCanvas[renderer.currentBlendMode]; - } - - if (this._mask) { - renderer.maskManager.pushMask(this._mask, renderer); - } - - var resolution = renderer.resolution; - context.setTransform( - transform.a * resolution, - transform.b * resolution, - transform.c * resolution, - transform.d * resolution, - transform.tx * resolution, - transform.ty * resolution - ); - - core.CanvasGraphics.renderGraphics(this, context); - - for (var i = 0, j = this.children.length; i < j; ++i) { - this.children[i].renderCanvas(renderer); - } - - if (this._mask) { - renderer.maskManager.popMask(renderer); - } - } -}; - -/** - * Retrieves the bounds of the graphic shape as a rectangle object - * - * @return {Rectangle} the rectangular bounding area - */ -Graphics.prototype.getBounds = function (matrix) { - // return an empty object if the item is a mask! - if (this.isMask) { - return core.math.Rectangle.EMPTY; - } - - if (this.dirty) { - this.updateLocalBounds(); - - this.glDirty = 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; - - return this._bounds; -}; - -/** - * Update the bounds of the object - * - */ -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 === core.CONST.SHAPES.RECT || type === core.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 === core.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 === core.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; -}; - -/** - * Generates the cached sprite when the sprite has cacheAsBitmap = true - * - * @private - */ -Graphics.prototype._generateCachedSprite = function () { - var bounds = this.getLocalBounds(); - - if (!this._cachedSprite) { - var canvasBuffer = new core.CanvasBuffer(bounds.width, bounds.height); - var texture = core.Texture.fromCanvas(canvasBuffer.canvas); - - this._cachedSprite = new core.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.. - core.CanvasGraphics.renderGraphics(this, this._cachedSprite.buffer.context); - - this._cachedSprite.alpha = this.alpha; -}; - -/** - * Updates texture size based on canvas size - * - * @private - */ -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. - * - */ -Graphics.prototype.destroyCachedSprite = function () { - this._cachedSprite.texture.destroy(true); - - // let the gc collect the unused sprite - // TODO could be object pooled! - this._cachedSprite = null; -}; - -/** - * 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. - */ -Graphics.prototype.drawShape = function (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 === core.CONST.SHAPES.POLY) { - data.shape.closed = this.filling; - this.currentPath = data; - } - - this.dirty = true; - - return data; -}; diff --git a/src/primitives/GraphicsData.js b/src/primitives/GraphicsData.js deleted file mode 100644 index 9377ef7..0000000 --- a/src/primitives/GraphicsData.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * A GraphicsData object. - * - * @class - * @namespace PIXI - */ -function GraphicsData(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; -} - -GraphicsData.prototype.constructor = GraphicsData; -module.exports = GraphicsData; diff --git a/src/primitives/index.js b/src/primitives/index.js deleted file mode 100644 index 7f02649..0000000 --- a/src/primitives/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @file Main export of the PIXI primitives library - * @author Mat Groves - * @copyright 2013-2015 GoodBoyDigital - * @license {@link https://github.com/GoodBoyDigital/pixi.js/blob/master/LICENSE|MIT License} - */ - -/** - * @namespace PIXI - */ -module.exports = { - Graphics: require('./Graphics'), - GraphicsData: require('./GraphicsData') -};